Merge pull request #326 from vector-im/feature/bma/push4
Notification update
This commit is contained in:
commit
d68e4bd4f0
17 changed files with 958 additions and 741 deletions
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.androidutils.system
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
@ -72,6 +73,17 @@ fun Context.getApplicationLabel(packageName: String): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true it the user has enabled the do not disturb mode.
|
||||||
|
*/
|
||||||
|
fun isDoNotDisturbModeOn(context: Context): Boolean {
|
||||||
|
// We cannot use NotificationManagerCompat here.
|
||||||
|
val setting = context.getSystemService<NotificationManager>()!!.currentInterruptionFilter
|
||||||
|
|
||||||
|
return setting == NotificationManager.INTERRUPTION_FILTER_NONE ||
|
||||||
|
setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* display the system dialog for granting this permission. If previously granted, the
|
* display the system dialog for granting this permission. If previously granted, the
|
||||||
* system will not show it (so you should call this method).
|
* system will not show it (so you should call this method).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,7 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId
|
||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
senderName = null,
|
senderName = null,
|
||||||
senderId = null,
|
senderId = null,
|
||||||
body = "$eventId in $roomId",
|
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
|
||||||
imageUriString = null,
|
imageUriString = null,
|
||||||
threadId = null,
|
threadId = null,
|
||||||
roomName = null,
|
roomName = null,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.push.impl.notifications
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
|
@ -51,4 +52,36 @@ class NotificationDisplayer @Inject constructor(
|
||||||
Timber.e(e, "## cancelAllNotifications() failed")
|
Timber.e(e, "## cancelAllNotifications() failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
|
fun displayDiagnosticNotification(notification: Notification) {
|
||||||
|
showNotificationMessage(
|
||||||
|
tag = "DIAGNOSTIC",
|
||||||
|
id = NOTIFICATION_ID_DIAGNOSTIC,
|
||||||
|
notification = notification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the foreground notification service.
|
||||||
|
*/
|
||||||
|
fun cancelNotificationForegroundService() {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
private const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID_DIAGNOSTIC = 888
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl.notifications
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
|
|
@ -26,8 +27,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||||
|
|
||||||
|
// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||||
class NotificationFactory @Inject constructor(
|
class NotificationFactory @Inject constructor(
|
||||||
private val notificationUtils: NotificationUtils,
|
private val notificationFactory: NotificationFactory,
|
||||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||||
) {
|
) {
|
||||||
|
|
@ -66,7 +68,7 @@ class NotificationFactory @Inject constructor(
|
||||||
when (processed) {
|
when (processed) {
|
||||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
|
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
|
||||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||||
notificationUtils.buildRoomInvitationNotification(event),
|
notificationFactory.createRoomInvitationNotification(event),
|
||||||
OneShotNotification.Append.Meta(
|
OneShotNotification.Append.Meta(
|
||||||
key = event.roomId.value,
|
key = event.roomId.value,
|
||||||
summaryLine = event.description,
|
summaryLine = event.description,
|
||||||
|
|
@ -84,7 +86,7 @@ class NotificationFactory @Inject constructor(
|
||||||
when (processed) {
|
when (processed) {
|
||||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||||
notificationUtils.buildSimpleEventNotification(event),
|
notificationFactory.createSimpleEventNotification(event),
|
||||||
OneShotNotification.Append.Meta(
|
OneShotNotification.Append.Meta(
|
||||||
key = event.eventId.value,
|
key = event.eventId.value,
|
||||||
summaryLine = event.description,
|
summaryLine = event.description,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.core.app.Person
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SessionId
|
||||||
import io.element.android.libraries.push.impl.R
|
import io.element.android.libraries.push.impl.R
|
||||||
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
import me.gujun.android.span.Span
|
import me.gujun.android.span.Span
|
||||||
|
|
@ -32,7 +33,7 @@ import javax.inject.Inject
|
||||||
class RoomGroupMessageCreator @Inject constructor(
|
class RoomGroupMessageCreator @Inject constructor(
|
||||||
private val bitmapLoader: NotificationBitmapLoader,
|
private val bitmapLoader: NotificationBitmapLoader,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val notificationUtils: NotificationUtils
|
private val notificationFactory: NotificationFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createRoomMessage(
|
fun createRoomMessage(
|
||||||
|
|
@ -43,7 +44,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
userAvatarUrl: String?
|
userAvatarUrl: String?
|
||||||
): RoomNotification.Message {
|
): RoomNotification.Message {
|
||||||
val lastKnownRoomEvent = events.last()
|
val lastKnownRoomEvent = events.last()
|
||||||
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: ""
|
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
|
||||||
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
||||||
val style = NotificationCompat.MessagingStyle(
|
val style = NotificationCompat.MessagingStyle(
|
||||||
Person.Builder()
|
Person.Builder()
|
||||||
|
|
@ -76,7 +77,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
shouldBing = events.any { it.noisy }
|
shouldBing = events.any { it.noisy }
|
||||||
)
|
)
|
||||||
return RoomNotification.Message(
|
return RoomNotification.Message(
|
||||||
notificationUtils.buildMessagesListNotification(
|
notificationFactory.createMessagesListNotification(
|
||||||
style,
|
style,
|
||||||
RoomEventGroupInfo(
|
RoomEventGroupInfo(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
@ -92,7 +93,6 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
threadId = lastKnownRoomEvent.threadId,
|
threadId = lastKnownRoomEvent.threadId,
|
||||||
largeIcon = largeBitmap,
|
largeIcon = largeBitmap,
|
||||||
lastMessageTimestamp,
|
lastMessageTimestamp,
|
||||||
userDisplayName,
|
|
||||||
tickerText
|
tickerText
|
||||||
),
|
),
|
||||||
meta
|
meta
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.app.Notification
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.impl.R
|
import io.element.android.libraries.push.impl.R
|
||||||
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
class SummaryGroupMessageCreator @Inject constructor(
|
class SummaryGroupMessageCreator @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val notificationUtils: NotificationUtils
|
private val notificationFactory: NotificationFactory
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createSummaryNotification(
|
fun createSummaryNotification(
|
||||||
|
|
@ -72,7 +73,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
||||||
// TODO get latest event?
|
// TODO get latest event?
|
||||||
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
|
||||||
return if (useCompleteNotificationFormat) {
|
return if (useCompleteNotificationFormat) {
|
||||||
notificationUtils.buildSummaryListNotification(
|
notificationFactory.createSummaryListNotification(
|
||||||
sessionId,
|
sessionId,
|
||||||
summaryInboxStyle,
|
summaryInboxStyle,
|
||||||
sumTitle,
|
sumTitle,
|
||||||
|
|
@ -165,7 +166,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
||||||
messageStr
|
messageStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return notificationUtils.buildSummaryListNotification(
|
return notificationFactory.createSummaryListNotification(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
style = null,
|
style = null,
|
||||||
compatSummary = privacyTitle,
|
compatSummary = privacyTitle,
|
||||||
|
|
|
||||||
|
|
@ -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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationUtils
|
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||||
|
|
@ -36,19 +36,19 @@ private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roo
|
||||||
|
|
||||||
class NotificationFactoryTest {
|
class NotificationFactoryTest {
|
||||||
|
|
||||||
private val notificationUtils = FakeNotificationUtils()
|
private val androidNotificationFactory = FakeAndroidNotificationFactory()
|
||||||
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
|
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
|
||||||
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
|
||||||
|
|
||||||
private val notificationFactory = NotificationFactory(
|
private val notificationFactory = NotificationFactory(
|
||||||
notificationUtils.instance,
|
androidNotificationFactory.instance,
|
||||||
roomGroupMessageCreator.instance,
|
roomGroupMessageCreator.instance,
|
||||||
summaryGroupMessageCreator.instance
|
summaryGroupMessageCreator.instance
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
|
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||||
val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT)
|
val expectedNotification = androidNotificationFactory.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT)
|
||||||
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT))
|
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT))
|
||||||
|
|
||||||
val result = roomInvitation.toNotifications()
|
val result = roomInvitation.toNotifications()
|
||||||
|
|
@ -85,7 +85,7 @@ class NotificationFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
|
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
|
||||||
val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
|
val expectedNotification = androidNotificationFactory.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
|
||||||
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT))
|
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT))
|
||||||
|
|
||||||
val result = roomInvitation.toNotifications()
|
val result = roomInvitation.toNotifications()
|
||||||
|
|
|
||||||
|
|
@ -17,24 +17,24 @@
|
||||||
package io.element.android.libraries.push.impl.notifications.fake
|
package io.element.android.libraries.push.impl.notifications.fake
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import io.element.android.libraries.push.impl.notifications.NotificationUtils
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
||||||
class FakeNotificationUtils {
|
class FakeAndroidNotificationFactory {
|
||||||
val instance = mockk<NotificationUtils>()
|
val instance = mockk<NotificationFactory>()
|
||||||
|
|
||||||
fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
|
fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
|
||||||
val mockNotification = mockk<Notification>()
|
val mockNotification = mockk<Notification>()
|
||||||
every { instance.buildRoomInvitationNotification(event) } returns mockNotification
|
every { instance.createRoomInvitationNotification(event) } returns mockNotification
|
||||||
return mockNotification
|
return mockNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification {
|
fun givenCreateSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification {
|
||||||
val mockNotification = mockk<Notification>()
|
val mockNotification = mockk<Notification>()
|
||||||
every { instance.buildSimpleEventNotification(event) } returns mockNotification
|
every { instance.createSimpleEventNotification(event) } returns mockNotification
|
||||||
return mockNotification
|
return mockNotification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue