Benoit Marty
1 year ago
committed by
GitHub
17 changed files with 958 additions and 741 deletions
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl |
||||
|
||||
object NotificationConfig { |
||||
// TODO EAx Implement and set to true at some point |
||||
const val supportMarkAsReadAction = false |
||||
|
||||
// TODO EAx Implement and set to true at some point |
||||
const val supportQuickReplyAction = false |
||||
} |
@ -1,718 +0,0 @@
@@ -1,718 +0,0 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
@file:Suppress("UNUSED_PARAMETER") |
||||
|
||||
package io.element.android.libraries.push.impl.notifications |
||||
|
||||
import android.Manifest |
||||
import android.annotation.SuppressLint |
||||
import android.app.Activity |
||||
import android.app.Notification |
||||
import android.app.NotificationChannel |
||||
import android.app.NotificationManager |
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.content.pm.PackageManager |
||||
import android.graphics.Bitmap |
||||
import android.graphics.Canvas |
||||
import android.os.Build |
||||
import androidx.annotation.ChecksSdkIntAtLeast |
||||
import androidx.annotation.DrawableRes |
||||
import androidx.core.app.ActivityCompat |
||||
import androidx.core.app.NotificationCompat |
||||
import androidx.core.app.NotificationManagerCompat |
||||
import androidx.core.app.RemoteInput |
||||
import androidx.core.content.ContextCompat |
||||
import androidx.core.content.getSystemService |
||||
import androidx.core.content.res.ResourcesCompat |
||||
import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.core.meta.BuildMeta |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.di.SingleIn |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.ThreadId |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.intent.IntentProvider |
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent |
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import timber.log.Timber |
||||
import javax.inject.Inject |
||||
|
||||
// TODO EAx Split into factories |
||||
@SingleIn(AppScope::class) |
||||
class NotificationUtils @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
// private val vectorPreferences: VectorPreferences, |
||||
private val stringProvider: StringProvider, |
||||
private val clock: SystemClock, |
||||
private val actionIds: NotificationActionIds, |
||||
private val intentProvider: IntentProvider, |
||||
private val buildMeta: BuildMeta, |
||||
) { |
||||
|
||||
companion object { |
||||
/* ========================================================================================== |
||||
* IDs for notifications |
||||
* ========================================================================================== */ |
||||
|
||||
/** |
||||
* Identifier of the foreground notification used to keep the application alive |
||||
* when it runs in background. |
||||
* This notification, which is not removable by the end user, displays what |
||||
* the application is doing while in background. |
||||
*/ |
||||
const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 |
||||
|
||||
/* ========================================================================================== |
||||
* IDs for channels |
||||
* ========================================================================================== */ |
||||
|
||||
// on devices >= android O, we need to define a channel for each notifications |
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" |
||||
|
||||
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" |
||||
|
||||
const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" |
||||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" |
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) |
||||
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
||||
|
||||
fun openSystemSettingsForSilentCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, SILENT_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
|
||||
fun openSystemSettingsForNoisyCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, NOISY_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
|
||||
fun openSystemSettingsForCallCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, CALL_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
} |
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context) |
||||
|
||||
init { |
||||
createNotificationChannels() |
||||
} |
||||
|
||||
/* ========================================================================================== |
||||
* Channel names |
||||
* ========================================================================================== */ |
||||
|
||||
/** |
||||
* Create notification channels. |
||||
*/ |
||||
private fun createNotificationChannels() { |
||||
if (!supportNotificationChannels()) { |
||||
return |
||||
} |
||||
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
|
||||
// Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE |
||||
// + currentTimeMillis). |
||||
// Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel |
||||
// Starting from this version the channel will not be dynamic |
||||
for (channel in notificationManager.notificationChannels) { |
||||
val channelId = channel.id |
||||
val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE" |
||||
if (channelId.startsWith(legacyBaseName)) { |
||||
notificationManager.deleteNotificationChannel(channelId) |
||||
} |
||||
} |
||||
// Migration - Remove deprecated channels |
||||
for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) { |
||||
notificationManager.getNotificationChannel(channelId)?.let { |
||||
notificationManager.deleteNotificationChannel(channelId) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Default notification importance: shows everywhere, makes noise, but does not visually |
||||
* intrude. |
||||
*/ |
||||
notificationManager.createNotificationChannel(NotificationChannel( |
||||
NOISY_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" }, |
||||
NotificationManager.IMPORTANCE_DEFAULT |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_noisy) |
||||
enableVibration(true) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
|
||||
/** |
||||
* Low notification importance: shows everywhere, but is not intrusive. |
||||
*/ |
||||
notificationManager.createNotificationChannel(NotificationChannel( |
||||
SILENT_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" }, |
||||
NotificationManager.IMPORTANCE_LOW |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_silent) |
||||
setSound(null, null) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
|
||||
notificationManager.createNotificationChannel(NotificationChannel( |
||||
LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_listening_for_events).ifEmpty { "Listening for events" }, |
||||
NotificationManager.IMPORTANCE_MIN |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_listening_for_events) |
||||
setSound(null, null) |
||||
setShowBadge(false) |
||||
}) |
||||
|
||||
notificationManager.createNotificationChannel(NotificationChannel( |
||||
CALL_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" }, |
||||
NotificationManager.IMPORTANCE_HIGH |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_call) |
||||
setSound(null, null) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
} |
||||
|
||||
fun getChannel(channelId: String): NotificationChannel? { |
||||
return notificationManager.getNotificationChannel(channelId) |
||||
} |
||||
|
||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? { |
||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
return getChannel(notificationChannel) |
||||
} |
||||
|
||||
/** |
||||
* Build a notification for a Room. |
||||
*/ |
||||
fun buildMessagesListNotification( |
||||
messageStyle: NotificationCompat.MessagingStyle, |
||||
roomInfo: RoomEventGroupInfo, |
||||
threadId: ThreadId?, |
||||
largeIcon: Bitmap?, |
||||
lastMessageTimestamp: Long, |
||||
senderDisplayNameForReplyCompat: String?, |
||||
tickerText: String |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
// Build the pending intent for when the notification is clicked |
||||
val openIntent = when { |
||||
threadId != null && |
||||
true |
||||
/** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ |
||||
-> buildOpenThreadIntent(roomInfo, threadId) |
||||
else -> buildOpenRoomIntent(roomInfo.sessionId, roomInfo.roomId) |
||||
} |
||||
|
||||
val smallIcon = R.drawable.ic_notification |
||||
|
||||
val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
return NotificationCompat.Builder(context, channelID) |
||||
.setOnlyAlertOnce(roomInfo.isUpdated) |
||||
.setWhen(lastMessageTimestamp) |
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices. |
||||
.setStyle(messageStyle) |
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings. |
||||
// For example, alarm notifications should display before promo notifications, or message from known contact |
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x) |
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE) |
||||
// ID of the corresponding shortcut, for conversation features under API 30+ |
||||
.setShortcutId(roomInfo.roomId.value) |
||||
// Title for API < 16 devices. |
||||
.setContentTitle(roomInfo.roomDisplayName) |
||||
// Content for API < 16 devices. |
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages)) |
||||
// Number of new notifications for API <24 (M and below) devices. |
||||
.setSubText( |
||||
stringProvider.getQuantityString( |
||||
R.plurals.notification_new_messages_for_room, |
||||
messageStyle.messages.size, |
||||
messageStyle.messages.size |
||||
) |
||||
) |
||||
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) |
||||
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID |
||||
.setGroup(roomInfo.sessionId.value) |
||||
// In order to avoid notification making sound twice (due to the summary notification) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
// Set primary color (important for Wear 2.0 Notifications). |
||||
.setColor(accentColor) |
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for |
||||
// 'importance' which is set in the NotificationChannel. The integers representing |
||||
// 'priority' are different from 'importance', so make sure you don't mix them. |
||||
.apply { |
||||
if (roomInfo.shouldBing) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
|
||||
// Add actions and notification intents |
||||
// Mark room as read |
||||
val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
markRoomReadIntent.action = actionIds.markRoomRead |
||||
markRoomReadIntent.data = createIgnoredUri("markRead?${roomInfo.sessionId}&$${roomInfo.roomId}") |
||||
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, roomInfo.sessionId) |
||||
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) |
||||
val markRoomReadPendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
markRoomReadIntent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
|
||||
NotificationCompat.Action.Builder( |
||||
R.drawable.ic_material_done_all_white, |
||||
stringProvider.getString(R.string.notification_room_action_mark_as_read), markRoomReadPendingIntent |
||||
) |
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) |
||||
.setShowsUserInterface(false) |
||||
.build() |
||||
.let { addAction(it) } |
||||
|
||||
// Quick reply |
||||
if (!roomInfo.hasSmartReplyError) { |
||||
buildQuickReplyIntent(roomInfo.sessionId, roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> |
||||
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) |
||||
.setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply)) |
||||
.build() |
||||
NotificationCompat.Action.Builder( |
||||
R.drawable.vector_notification_quick_reply, |
||||
stringProvider.getString(R.string.notification_room_action_quick_reply), replyPendingIntent |
||||
) |
||||
.addRemoteInput(remoteInput) |
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) |
||||
.setShowsUserInterface(false) |
||||
.build() |
||||
.let { addAction(it) } |
||||
} |
||||
} |
||||
|
||||
if (openIntent != null) { |
||||
setContentIntent(openIntent) |
||||
} |
||||
|
||||
if (largeIcon != null) { |
||||
setLargeIcon(largeIcon) |
||||
} |
||||
|
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.dismissRoom |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, roomInfo.sessionId) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId) |
||||
val pendingIntent = PendingIntent.getBroadcast( |
||||
context.applicationContext, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
setDeleteIntent(pendingIntent) |
||||
} |
||||
.setTicker(tickerText) |
||||
.build() |
||||
} |
||||
|
||||
fun buildRoomInvitationNotification( |
||||
inviteNotifiableEvent: InviteNotifiableEvent |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
// Build the pending intent for when the notification is clicked |
||||
val smallIcon = R.drawable.ic_notification |
||||
val channelID = if (inviteNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
|
||||
return NotificationCompat.Builder(context, channelID) |
||||
.setOnlyAlertOnce(true) |
||||
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) |
||||
.setContentText(inviteNotifiableEvent.description) |
||||
.setGroup(inviteNotifiableEvent.sessionId.value) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
.setColor(accentColor) |
||||
.apply { |
||||
val roomId = inviteNotifiableEvent.roomId |
||||
// offer to type a quick reject button |
||||
val rejectIntent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
rejectIntent.action = actionIds.reject |
||||
rejectIntent.data = createIgnoredUri("rejectInvite?${inviteNotifiableEvent.sessionId}&$roomId") |
||||
rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, inviteNotifiableEvent.sessionId) |
||||
rejectIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
val rejectIntentPendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
rejectIntent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
|
||||
addAction( |
||||
R.drawable.vector_notification_reject_invitation, |
||||
stringProvider.getString(R.string.notification_invitation_action_reject), |
||||
rejectIntentPendingIntent |
||||
) |
||||
|
||||
// offer to type a quick accept button |
||||
val joinIntent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
joinIntent.action = actionIds.join |
||||
joinIntent.data = createIgnoredUri("acceptInvite?${inviteNotifiableEvent.sessionId}&$roomId") |
||||
joinIntent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, inviteNotifiableEvent.sessionId) |
||||
joinIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
val joinIntentPendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
joinIntent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
addAction( |
||||
R.drawable.vector_notification_accept_invitation, |
||||
stringProvider.getString(R.string.notification_invitation_action_join), |
||||
joinIntentPendingIntent |
||||
) |
||||
|
||||
/* |
||||
val contentIntent = HomeActivity.newIntent( |
||||
context, |
||||
firstStartMainActivity = true, |
||||
inviteNotificationRoomId = inviteNotifiableEvent.roomId |
||||
) |
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP |
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that |
||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId) |
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE)) |
||||
|
||||
*/ |
||||
|
||||
if (inviteNotifiableEvent.noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
|
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
setAutoCancel(true) |
||||
} |
||||
.build() |
||||
} |
||||
|
||||
fun buildSimpleEventNotification( |
||||
simpleNotifiableEvent: SimpleNotifiableEvent, |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
// Build the pending intent for when the notification is clicked |
||||
val smallIcon = R.drawable.ic_notification |
||||
|
||||
val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
|
||||
return NotificationCompat.Builder(context, channelID) |
||||
.setOnlyAlertOnce(true) |
||||
.setContentTitle(buildMeta.applicationName) |
||||
.setContentText(simpleNotifiableEvent.description) |
||||
.setGroup(simpleNotifiableEvent.sessionId.value) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
.setColor(accentColor) |
||||
.setAutoCancel(true) |
||||
.apply { |
||||
/* TODO EAx |
||||
val contentIntent = HomeActivity.newIntent(context, firstStartMainActivity = true) |
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP |
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that |
||||
contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId) |
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE)) |
||||
*/ |
||||
if (simpleNotifiableEvent.noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
|
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
setAutoCancel(true) |
||||
} |
||||
.build() |
||||
} |
||||
|
||||
private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? { |
||||
val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = null) |
||||
return PendingIntent.getActivity( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? { |
||||
val sessionId = roomInfo.sessionId |
||||
val roomId = roomInfo.roomId |
||||
val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) |
||||
return PendingIntent.getActivity( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent { |
||||
val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = null, threadId = null) |
||||
return PendingIntent.getActivity( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
/* |
||||
Direct reply is new in Android N, and Android already handles the UI, so the right pending intent |
||||
here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver, |
||||
which runs on the UI thread. It also works without unlocking, making the process really fluid for the user. |
||||
However, for Android devices running Marshmallow and below (API level 23 and below), |
||||
it will be more appropriate to use an activity. Since you have to provide your own UI. |
||||
*/ |
||||
private fun buildQuickReplyIntent( |
||||
sessionId: SessionId, |
||||
roomId: RoomId, |
||||
threadId: ThreadId?, |
||||
senderName: String? |
||||
): PendingIntent? { |
||||
val intent: Intent |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.smartReply |
||||
intent.data = createIgnoredUri("quickReply?$sessionId&$roomId") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
threadId?.let { |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it) |
||||
} |
||||
|
||||
return PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
// PendingIntents attached to actions with remote inputs must be mutable |
||||
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
||||
PendingIntent.FLAG_MUTABLE |
||||
} else { |
||||
0 |
||||
} |
||||
) |
||||
} else { |
||||
/* |
||||
TODO |
||||
if (!LockScreenActivity.isDisplayingALockScreenActivity()) { |
||||
// start your activity for Android M and below |
||||
val quickReplyIntent = Intent(context, LockScreenActivity::class.java) |
||||
quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId) |
||||
quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, senderName ?: "") |
||||
|
||||
// the action must be unique else the parameters are ignored |
||||
quickReplyIntent.action = QUICK_LAUNCH_ACTION |
||||
quickReplyIntent.data = createIgnoredUri($roomId") |
||||
return PendingIntent.getActivity(context, 0, quickReplyIntent, PendingIntentCompat.FLAG_IMMUTABLE) |
||||
} |
||||
*/ |
||||
} |
||||
return null |
||||
} |
||||
|
||||
// // Number of new notifications for API <24 (M and below) devices. |
||||
/** |
||||
* Build the summary notification. |
||||
*/ |
||||
fun buildSummaryListNotification( |
||||
sessionId: SessionId, |
||||
style: NotificationCompat.InboxStyle?, |
||||
compatSummary: String, |
||||
noisy: Boolean, |
||||
lastMessageTimestamp: Long |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
val smallIcon = R.drawable.ic_notification |
||||
|
||||
return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) |
||||
.setOnlyAlertOnce(true) |
||||
// used in compat < N, after summary is built based on child notifications |
||||
.setWhen(lastMessageTimestamp) |
||||
.setStyle(style) |
||||
.setContentTitle(sessionId.value) |
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE) |
||||
.setSmallIcon(smallIcon) |
||||
// set content text to support devices running API level < 24 |
||||
.setContentText(compatSummary) |
||||
.setGroup(sessionId.value) |
||||
// set this notification as the summary for the group |
||||
.setGroupSummary(true) |
||||
.setColor(accentColor) |
||||
.apply { |
||||
if (noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
// compat |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
} |
||||
.setContentIntent(buildOpenHomePendingIntentForSummary(sessionId)) |
||||
.setDeleteIntent(getDismissSummaryPendingIntent(sessionId)) |
||||
.build() |
||||
} |
||||
|
||||
private fun getDismissSummaryPendingIntent(sessionId: SessionId): PendingIntent { |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.dismissSummary |
||||
intent.data = createIgnoredUri("deleteSummary?$sessionId") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) |
||||
return PendingIntent.getBroadcast( |
||||
context.applicationContext, |
||||
0, |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Cancel the foreground notification service. |
||||
*/ |
||||
fun cancelNotificationForegroundService() { |
||||
notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) |
||||
} |
||||
|
||||
/** |
||||
* Cancel all the notification. |
||||
*/ |
||||
fun cancelAllNotifications() { |
||||
// Keep this try catch (reported by GA) |
||||
try { |
||||
notificationManager.cancelAll() |
||||
} catch (e: Exception) { |
||||
Timber.e(e, "## cancelAllNotifications() failed") |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("LaunchActivityFromNotification") |
||||
fun displayDiagnosticNotification() { |
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { |
||||
Timber.w("Not allowed to notify.") |
||||
return |
||||
} |
||||
val testActionIntent = Intent(context, TestNotificationReceiver::class.java) |
||||
testActionIntent.action = actionIds.diagnostic |
||||
val testPendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
0, |
||||
testActionIntent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
notificationManager.notify( |
||||
"DIAGNOSTIC", |
||||
888, |
||||
NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) |
||||
.setContentTitle(buildMeta.applicationName) |
||||
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) |
||||
.setSmallIcon(R.drawable.ic_notification) |
||||
.setLargeIcon(getBitmap(context, R.drawable.element_logo_green)) |
||||
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) |
||||
.setPriority(NotificationCompat.PRIORITY_MAX) |
||||
.setCategory(NotificationCompat.CATEGORY_STATUS) |
||||
.setAutoCancel(true) |
||||
.setContentIntent(testPendingIntent) |
||||
.build() |
||||
) |
||||
} |
||||
|
||||
private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { |
||||
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null |
||||
val canvas = Canvas() |
||||
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) |
||||
canvas.setBitmap(bitmap) |
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) |
||||
drawable.draw(canvas) |
||||
return bitmap |
||||
} |
||||
|
||||
/** |
||||
* Return true it the user has enabled the do not disturb mode. |
||||
*/ |
||||
fun isDoNotDisturbModeOn(): Boolean { |
||||
// We cannot use NotificationManagerCompat here. |
||||
val setting = context.getSystemService<NotificationManager>()!!.currentInterruptionFilter |
||||
|
||||
return setting == NotificationManager.INTERRUPTION_FILTER_NONE || |
||||
setting == NotificationManager.INTERRUPTION_FILTER_ALARMS |
||||
} |
||||
|
||||
/* |
||||
private fun getActionText(@StringRes stringRes: Int, @AttrRes colorRes: Int): Spannable { |
||||
return SpannableString(context.getText(stringRes)).apply { |
||||
val foregroundColorSpan = ForegroundColorSpan(ThemeUtils.getColor(context, colorRes)) |
||||
setSpan(foregroundColorSpan, 0, length, 0) |
||||
} |
||||
} |
||||
*/ |
||||
|
||||
private fun ensureTitleNotEmpty(title: String?): CharSequence { |
||||
if (title.isNullOrBlank()) { |
||||
return buildMeta.applicationName |
||||
} |
||||
|
||||
return title |
||||
} |
||||
} |
@ -0,0 +1,179 @@
@@ -0,0 +1,179 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.channels |
||||
|
||||
import android.app.Activity |
||||
import android.app.NotificationChannel |
||||
import android.app.NotificationManager |
||||
import android.content.Context |
||||
import android.os.Build |
||||
import androidx.annotation.ChecksSdkIntAtLeast |
||||
import androidx.core.app.NotificationManagerCompat |
||||
import androidx.core.content.ContextCompat |
||||
import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.di.SingleIn |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* on devices >= android O, we need to define a channel for each notifications. |
||||
*/ |
||||
@SingleIn(AppScope::class) |
||||
class NotificationChannels @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val stringProvider: StringProvider, |
||||
) { |
||||
private val notificationManager = NotificationManagerCompat.from(context) |
||||
|
||||
init { |
||||
createNotificationChannels() |
||||
} |
||||
|
||||
/* ========================================================================================== |
||||
* Channel names |
||||
* ========================================================================================== */ |
||||
|
||||
/** |
||||
* Create notification channels. |
||||
*/ |
||||
private fun createNotificationChannels() { |
||||
if (!supportNotificationChannels()) { |
||||
return |
||||
} |
||||
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
|
||||
// Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE |
||||
// + currentTimeMillis). |
||||
// Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel |
||||
// Starting from this version the channel will not be dynamic |
||||
for (channel in notificationManager.notificationChannels) { |
||||
val channelId = channel.id |
||||
val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE" |
||||
if (channelId.startsWith(legacyBaseName)) { |
||||
notificationManager.deleteNotificationChannel(channelId) |
||||
} |
||||
} |
||||
// Migration - Remove deprecated channels |
||||
for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) { |
||||
notificationManager.getNotificationChannel(channelId)?.let { |
||||
notificationManager.deleteNotificationChannel(channelId) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Default notification importance: shows everywhere, makes noise, but does not visually |
||||
* intrude. |
||||
*/ |
||||
notificationManager.createNotificationChannel( |
||||
NotificationChannel( |
||||
NOISY_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" }, |
||||
NotificationManager.IMPORTANCE_DEFAULT |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_noisy) |
||||
enableVibration(true) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
|
||||
/** |
||||
* Low notification importance: shows everywhere, but is not intrusive. |
||||
*/ |
||||
notificationManager.createNotificationChannel( |
||||
NotificationChannel( |
||||
SILENT_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" }, |
||||
NotificationManager.IMPORTANCE_LOW |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_silent) |
||||
setSound(null, null) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
|
||||
notificationManager.createNotificationChannel( |
||||
NotificationChannel( |
||||
LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_listening_for_events).ifEmpty { "Listening for events" }, |
||||
NotificationManager.IMPORTANCE_MIN |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_listening_for_events) |
||||
setSound(null, null) |
||||
setShowBadge(false) |
||||
}) |
||||
|
||||
notificationManager.createNotificationChannel( |
||||
NotificationChannel( |
||||
CALL_NOTIFICATION_CHANNEL_ID, |
||||
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" }, |
||||
NotificationManager.IMPORTANCE_HIGH |
||||
) |
||||
.apply { |
||||
description = stringProvider.getString(R.string.notification_channel_call) |
||||
setSound(null, null) |
||||
enableLights(true) |
||||
lightColor = accentColor |
||||
}) |
||||
} |
||||
|
||||
private fun getChannel(channelId: String): NotificationChannel? { |
||||
return notificationManager.getNotificationChannel(channelId) |
||||
} |
||||
|
||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? { |
||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
return getChannel(notificationChannel) |
||||
} |
||||
|
||||
fun getChannelIdForMessage(noisy: Boolean): String { |
||||
return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID |
||||
} |
||||
|
||||
fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID |
||||
|
||||
companion object { |
||||
/* ========================================================================================== |
||||
* IDs for channels |
||||
* ========================================================================================== */ |
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" |
||||
private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" |
||||
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" |
||||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" |
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) |
||||
private fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
||||
|
||||
fun openSystemSettingsForSilentCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, SILENT_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
|
||||
fun openSystemSettingsForNoisyCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, NOISY_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
|
||||
fun openSystemSettingsForCallCategory(activity: Activity) { |
||||
startNotificationChannelSettingsIntent(activity, CALL_NOTIFICATION_CHANNEL_ID) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,295 @@
@@ -0,0 +1,295 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories |
||||
|
||||
import android.app.Notification |
||||
import android.content.Context |
||||
import android.graphics.Bitmap |
||||
import android.graphics.Canvas |
||||
import androidx.annotation.DrawableRes |
||||
import androidx.core.app.NotificationCompat |
||||
import androidx.core.content.ContextCompat |
||||
import androidx.core.content.res.ResourcesCompat |
||||
import io.element.android.libraries.core.meta.BuildMeta |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.ThreadId |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo |
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels |
||||
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory |
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory |
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory |
||||
import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory |
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent |
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import javax.inject.Inject |
||||
|
||||
class NotificationFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val notificationChannels: NotificationChannels, |
||||
private val stringProvider: StringProvider, |
||||
private val buildMeta: BuildMeta, |
||||
private val pendingIntentFactory: PendingIntentFactory, |
||||
private val markAsReadActionFactory: MarkAsReadActionFactory, |
||||
private val quickReplyActionFactory: QuickReplyActionFactory, |
||||
private val rejectInvitationActionFactory: RejectInvitationActionFactory, |
||||
private val acceptInvitationActionFactory: AcceptInvitationActionFactory, |
||||
) { |
||||
/** |
||||
* Create a notification for a Room. |
||||
*/ |
||||
fun createMessagesListNotification( |
||||
messageStyle: NotificationCompat.MessagingStyle, |
||||
roomInfo: RoomEventGroupInfo, |
||||
threadId: ThreadId?, |
||||
largeIcon: Bitmap?, |
||||
lastMessageTimestamp: Long, |
||||
tickerText: String |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
// Build the pending intent for when the notification is clicked |
||||
val openIntent = when { |
||||
threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId) |
||||
else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId) |
||||
} |
||||
|
||||
val smallIcon = R.drawable.ic_notification |
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing) |
||||
return NotificationCompat.Builder(context, channelId) |
||||
.setOnlyAlertOnce(roomInfo.isUpdated) |
||||
.setWhen(lastMessageTimestamp) |
||||
// MESSAGING_STYLE sets title and content for API 16 and above devices. |
||||
.setStyle(messageStyle) |
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings. |
||||
// For example, alarm notifications should display before promo notifications, or message from known contact |
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x) |
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE) |
||||
// ID of the corresponding shortcut, for conversation features under API 30+ |
||||
.setShortcutId(roomInfo.roomId.value) |
||||
// Title for API < 16 devices. |
||||
.setContentTitle(roomInfo.roomDisplayName) |
||||
// Content for API < 16 devices. |
||||
.setContentText(stringProvider.getString(R.string.notification_new_messages)) |
||||
// Number of new notifications for API <24 (M and below) devices. |
||||
.setSubText( |
||||
stringProvider.getQuantityString( |
||||
R.plurals.notification_new_messages_for_room, |
||||
messageStyle.messages.size, |
||||
messageStyle.messages.size |
||||
) |
||||
) |
||||
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) |
||||
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID |
||||
.setGroup(roomInfo.sessionId.value) |
||||
// In order to avoid notification making sound twice (due to the summary notification) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
// Set primary color (important for Wear 2.0 Notifications). |
||||
.setColor(accentColor) |
||||
// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for |
||||
// 'importance' which is set in the NotificationChannel. The integers representing |
||||
// 'priority' are different from 'importance', so make sure you don't mix them. |
||||
.apply { |
||||
if (roomInfo.shouldBing) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
|
||||
// Add actions and notification intents |
||||
// Mark room as read |
||||
addAction(markAsReadActionFactory.create(roomInfo)) |
||||
// Quick reply |
||||
if (!roomInfo.hasSmartReplyError) { |
||||
addAction(quickReplyActionFactory.create(roomInfo, threadId)) |
||||
} |
||||
if (openIntent != null) { |
||||
setContentIntent(openIntent) |
||||
} |
||||
if (largeIcon != null) { |
||||
setLargeIcon(largeIcon) |
||||
} |
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) |
||||
} |
||||
.setTicker(tickerText) |
||||
.build() |
||||
} |
||||
|
||||
fun createRoomInvitationNotification( |
||||
inviteNotifiableEvent: InviteNotifiableEvent |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
val smallIcon = R.drawable.ic_notification |
||||
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) |
||||
return NotificationCompat.Builder(context, channelId) |
||||
.setOnlyAlertOnce(true) |
||||
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) |
||||
.setContentText(inviteNotifiableEvent.description) |
||||
.setGroup(inviteNotifiableEvent.sessionId.value) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
.setColor(accentColor) |
||||
.addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) |
||||
.addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) |
||||
.apply { |
||||
/* |
||||
// Build the pending intent for when the notification is clicked |
||||
val contentIntent = HomeActivity.newIntent( |
||||
context, |
||||
firstStartMainActivity = true, |
||||
inviteNotificationRoomId = inviteNotifiableEvent.roomId |
||||
) |
||||
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP |
||||
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that |
||||
contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId) |
||||
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE)) |
||||
|
||||
*/ |
||||
|
||||
if (inviteNotifiableEvent.noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
setAutoCancel(true) |
||||
} |
||||
.build() |
||||
} |
||||
|
||||
fun createSimpleEventNotification( |
||||
simpleNotifiableEvent: SimpleNotifiableEvent, |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
val smallIcon = R.drawable.ic_notification |
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) |
||||
return NotificationCompat.Builder(context, channelId) |
||||
.setOnlyAlertOnce(true) |
||||
.setContentTitle(buildMeta.applicationName) |
||||
.setContentText(simpleNotifiableEvent.description) |
||||
.setGroup(simpleNotifiableEvent.sessionId.value) |
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) |
||||
.setSmallIcon(smallIcon) |
||||
.setColor(accentColor) |
||||
.setAutoCancel(true) |
||||
.setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId)) |
||||
.apply { |
||||
if (simpleNotifiableEvent.noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
setAutoCancel(true) |
||||
} |
||||
.build() |
||||
} |
||||
|
||||
/** |
||||
* Create the summary notification. |
||||
*/ |
||||
fun createSummaryListNotification( |
||||
sessionId: SessionId, |
||||
style: NotificationCompat.InboxStyle?, |
||||
compatSummary: String, |
||||
noisy: Boolean, |
||||
lastMessageTimestamp: Long |
||||
): Notification { |
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) |
||||
val smallIcon = R.drawable.ic_notification |
||||
val channelId = notificationChannels.getChannelIdForMessage(noisy) |
||||
return NotificationCompat.Builder(context, channelId) |
||||
.setOnlyAlertOnce(true) |
||||
// used in compat < N, after summary is built based on child notifications |
||||
.setWhen(lastMessageTimestamp) |
||||
.setStyle(style) |
||||
.setContentTitle(sessionId.value) |
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE) |
||||
.setSmallIcon(smallIcon) |
||||
// set content text to support devices running API level < 24 |
||||
.setContentText(compatSummary) |
||||
.setGroup(sessionId.value) |
||||
// set this notification as the summary for the group |
||||
.setGroupSummary(true) |
||||
.setColor(accentColor) |
||||
.apply { |
||||
if (noisy) { |
||||
// Compat |
||||
priority = NotificationCompat.PRIORITY_DEFAULT |
||||
/* |
||||
vectorPreferences.getNotificationRingTone()?.let { |
||||
setSound(it) |
||||
} |
||||
*/ |
||||
setLights(accentColor, 500, 500) |
||||
} else { |
||||
// compat |
||||
priority = NotificationCompat.PRIORITY_LOW |
||||
} |
||||
} |
||||
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId)) |
||||
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId)) |
||||
.build() |
||||
} |
||||
|
||||
fun createDiagnosticNotification(): Notification { |
||||
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) |
||||
.setContentTitle(buildMeta.applicationName) |
||||
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) |
||||
.setSmallIcon(R.drawable.ic_notification) |
||||
.setLargeIcon(getBitmap(R.drawable.element_logo_green)) |
||||
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) |
||||
.setPriority(NotificationCompat.PRIORITY_MAX) |
||||
.setCategory(NotificationCompat.CATEGORY_STATUS) |
||||
.setAutoCancel(true) |
||||
.setContentIntent(pendingIntentFactory.createTestPendingIntent()) |
||||
.build() |
||||
} |
||||
|
||||
private fun getBitmap(@DrawableRes drawableRes: Int): Bitmap? { |
||||
val drawable = ResourcesCompat.getDrawable(context.resources, drawableRes, null) ?: return null |
||||
val canvas = Canvas() |
||||
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) |
||||
canvas.setBitmap(bitmap) |
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) |
||||
drawable.draw(canvas) |
||||
return bitmap |
||||
} |
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.ThreadId |
||||
import io.element.android.libraries.push.impl.intent.IntentProvider |
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds |
||||
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver |
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo |
||||
import io.element.android.libraries.push.impl.notifications.TestNotificationReceiver |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import javax.inject.Inject |
||||
|
||||
class PendingIntentFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val intentProvider: IntentProvider, |
||||
private val clock: SystemClock, |
||||
private val actionIds: NotificationActionIds, |
||||
) { |
||||
fun createOpenSessionPendingIntent(sessionId: SessionId): PendingIntent? { |
||||
return createPendingIntent(sessionId = sessionId, roomId = null, threadId = null) |
||||
} |
||||
|
||||
fun createOpenRoomPendingIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? { |
||||
return createPendingIntent(sessionId = sessionId, roomId = roomId, threadId = null) |
||||
} |
||||
|
||||
fun createOpenThreadPendingIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? { |
||||
return createPendingIntent(sessionId = roomInfo.sessionId, roomId = roomInfo.roomId, threadId = threadId) |
||||
} |
||||
|
||||
private fun createPendingIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): PendingIntent? { |
||||
val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) |
||||
return PendingIntent.getActivity( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
fun createDismissSummaryPendingIntent(sessionId: SessionId): PendingIntent { |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.dismissSummary |
||||
intent.data = createIgnoredUri("deleteSummary/${sessionId.value}") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) |
||||
return PendingIntent.getBroadcast( |
||||
context, |
||||
0, |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
fun createDismissRoomPendingIntent(sessionId: SessionId, roomId: RoomId): PendingIntent { |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.dismissRoom |
||||
intent.data = createIgnoredUri("deleteRoom/${sessionId.value}/${roomId.value}") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value) |
||||
return PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
|
||||
fun createTestPendingIntent(): PendingIntent? { |
||||
val testActionIntent = Intent(context, TestNotificationReceiver::class.java) |
||||
testActionIntent.action = actionIds.diagnostic |
||||
return PendingIntent.getBroadcast( |
||||
context, |
||||
0, |
||||
testActionIntent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories.action |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import androidx.core.app.NotificationCompat |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds |
||||
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver |
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import javax.inject.Inject |
||||
|
||||
class AcceptInvitationActionFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val actionIds: NotificationActionIds, |
||||
private val stringProvider: StringProvider, |
||||
private val clock: SystemClock, |
||||
) { |
||||
// offer to type a quick accept button |
||||
fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action { |
||||
val sessionId = inviteNotifiableEvent.sessionId.value |
||||
val roomId = inviteNotifiableEvent.roomId.value |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.join |
||||
intent.data = createIgnoredUri("acceptInvite/$sessionId/$roomId") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
val pendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
return NotificationCompat.Action.Builder( |
||||
R.drawable.vector_notification_accept_invitation, |
||||
stringProvider.getString(R.string.notification_invitation_action_join), |
||||
pendingIntent |
||||
).build() |
||||
} |
||||
} |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories.action |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import androidx.core.app.NotificationCompat |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.push.impl.NotificationConfig |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds |
||||
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver |
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import javax.inject.Inject |
||||
|
||||
class MarkAsReadActionFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val actionIds: NotificationActionIds, |
||||
private val stringProvider: StringProvider, |
||||
private val clock: SystemClock, |
||||
) { |
||||
fun create(roomInfo: RoomEventGroupInfo): NotificationCompat.Action? { |
||||
if (!NotificationConfig.supportMarkAsReadAction) return null |
||||
val sessionId = roomInfo.sessionId.value |
||||
val roomId = roomInfo.roomId.value |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.markRoomRead |
||||
intent.data = createIgnoredUri("markRead/$sessionId/$roomId") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
val pendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
|
||||
return NotificationCompat.Action.Builder( |
||||
R.drawable.ic_material_done_all_white, |
||||
stringProvider.getString(R.string.notification_room_action_mark_as_read), |
||||
pendingIntent |
||||
) |
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) |
||||
.setShowsUserInterface(false) |
||||
.build() |
||||
} |
||||
} |
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories.action |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Build |
||||
import androidx.core.app.NotificationCompat |
||||
import androidx.core.app.RemoteInput |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.SessionId |
||||
import io.element.android.libraries.matrix.api.core.ThreadId |
||||
import io.element.android.libraries.push.impl.NotificationConfig |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds |
||||
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver |
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import javax.inject.Inject |
||||
|
||||
class QuickReplyActionFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val actionIds: NotificationActionIds, |
||||
private val stringProvider: StringProvider, |
||||
private val clock: SystemClock, |
||||
) { |
||||
fun create(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): NotificationCompat.Action? { |
||||
if (!NotificationConfig.supportQuickReplyAction) return null |
||||
val sessionId = roomInfo.sessionId |
||||
val roomId = roomInfo.roomId |
||||
return buildQuickReplyIntent(sessionId, roomId, threadId)?.let { replyPendingIntent -> |
||||
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) |
||||
.setLabel(stringProvider.getString(R.string.notification_room_action_quick_reply)) |
||||
.build() |
||||
|
||||
NotificationCompat.Action.Builder( |
||||
R.drawable.vector_notification_quick_reply, |
||||
stringProvider.getString(R.string.notification_room_action_quick_reply), |
||||
replyPendingIntent |
||||
) |
||||
.addRemoteInput(remoteInput) |
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) |
||||
.setShowsUserInterface(false) |
||||
.build() |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Direct reply is new in Android N, and Android already handles the UI, so the right pending intent |
||||
* here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver, |
||||
* which runs on the UI thread. It also works without unlocking, making the process really fluid for the user. |
||||
* However, for Android devices running Marshmallow and below (API level 23 and below), |
||||
* it will be more appropriate to use an activity. Since you have to provide your own UI. |
||||
*/ |
||||
private fun buildQuickReplyIntent( |
||||
sessionId: SessionId, |
||||
roomId: RoomId, |
||||
threadId: ThreadId?, |
||||
): PendingIntent? { |
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.smartReply |
||||
intent.data = createIgnoredUri("quickReply/${sessionId.value}/${roomId.value}" + threadId?.let { "/${it.value}" }.orEmpty()) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value) |
||||
threadId?.let { |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) |
||||
} |
||||
|
||||
PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
// PendingIntents attached to actions with remote inputs must be mutable |
||||
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
||||
PendingIntent.FLAG_MUTABLE |
||||
} else { |
||||
0 |
||||
} |
||||
) |
||||
} else { |
||||
null |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories.action |
||||
|
||||
import android.app.PendingIntent |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import androidx.core.app.NotificationCompat |
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri |
||||
import io.element.android.libraries.di.ApplicationContext |
||||
import io.element.android.libraries.push.impl.R |
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds |
||||
import io.element.android.libraries.push.impl.notifications.NotificationBroadcastReceiver |
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent |
||||
import io.element.android.services.toolbox.api.strings.StringProvider |
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock |
||||
import javax.inject.Inject |
||||
|
||||
class RejectInvitationActionFactory @Inject constructor( |
||||
@ApplicationContext private val context: Context, |
||||
private val actionIds: NotificationActionIds, |
||||
private val stringProvider: StringProvider, |
||||
private val clock: SystemClock, |
||||
) { |
||||
fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action? { |
||||
val sessionId = inviteNotifiableEvent.sessionId.value |
||||
val roomId = inviteNotifiableEvent.roomId.value |
||||
val intent = Intent(context, NotificationBroadcastReceiver::class.java) |
||||
intent.action = actionIds.reject |
||||
intent.data = createIgnoredUri("rejectInvite/$sessionId/$roomId") |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) |
||||
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) |
||||
val pendingIntent = PendingIntent.getBroadcast( |
||||
context, |
||||
clock.epochMillis().toInt(), |
||||
intent, |
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE |
||||
) |
||||
|
||||
return NotificationCompat.Action.Builder( |
||||
R.drawable.vector_notification_reject_invitation, |
||||
stringProvider.getString(R.string.notification_invitation_action_reject), |
||||
pendingIntent |
||||
).build() |
||||
} |
||||
} |
Loading…
Reference in new issue