From 65717364ca726fa3e7abc357c19063f1c182c160 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 10:32:20 +0200 Subject: [PATCH 01/14] Cleanup --- .../impl/notifications/NotificationUtils.kt | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 8aeaa998ca..34f27cb62a 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -355,7 +355,6 @@ class NotificationUtils @Inject constructor( 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 @@ -407,6 +406,7 @@ class NotificationUtils @Inject constructor( ) /* + // Build the pending intent for when the notification is clicked val contentIntent = HomeActivity.newIntent( context, firstStartMainActivity = true, @@ -441,7 +441,6 @@ class NotificationUtils @Inject constructor( 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 @@ -455,14 +454,8 @@ class NotificationUtils @Inject constructor( .setSmallIcon(smallIcon) .setColor(accentColor) .setAutoCancel(true) + .setContentIntent(buildOpenRoomIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId)) .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 @@ -481,30 +474,20 @@ class NotificationUtils @Inject constructor( .build() } + private fun buildOpenSessionIntent(sessionId: SessionId): PendingIntent? { + return getPendingIntent(sessionId = sessionId, roomId = null, threadId = null) + } + 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 - ) + return getPendingIntent(sessionId = sessionId, roomId = roomId, threadId = null) } 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 - ) + return getPendingIntent(sessionId = roomInfo.sessionId, roomId = roomInfo.roomId, threadId = threadId) } - private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent { - val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = null, threadId = null) + private fun getPendingIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): PendingIntent? { + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), @@ -567,7 +550,6 @@ class NotificationUtils @Inject constructor( return null } - // // Number of new notifications for API <24 (M and below) devices. /** * Build the summary notification. */ @@ -610,7 +592,7 @@ class NotificationUtils @Inject constructor( priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(buildOpenHomePendingIntentForSummary(sessionId)) + .setContentIntent(buildOpenSessionIntent(sessionId)) .setDeleteIntent(getDismissSummaryPendingIntent(sessionId)) .build() } From cb8d206ed4bf6bfaab726e1b3a64fc6467658b2a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:09:42 +0200 Subject: [PATCH 02/14] Extract code which create notification actions into ActionFactory and disable quick reply and mark as read actions. --- .../android/libraries/push/impl/Config.kt | 25 +++ .../impl/notifications/NotificationUtils.kt | 142 ++---------------- .../notifications/RoomGroupMessageCreator.kt | 1 - .../actions/AcceptInvitationActionFactory.kt | 60 ++++++++ .../actions/MarkAsReadActionFactory.kt | 65 ++++++++ .../actions/QuickReplyActionFactory.kt | 103 +++++++++++++ .../actions/RejectInvitationActionFactory.kt | 60 ++++++++ 7 files changed, 326 insertions(+), 130 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt new file mode 100644 index 0000000000..eb37332f8f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt @@ -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 +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 34f27cb62a..ef4d4c7438 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -36,7 +36,6 @@ 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 @@ -51,6 +50,10 @@ 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.actions.AcceptInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.actions.MarkAsReadActionFactory +import io.element.android.libraries.push.impl.notifications.actions.QuickReplyActionFactory +import io.element.android.libraries.push.impl.notifications.actions.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 @@ -68,6 +71,10 @@ class NotificationUtils @Inject constructor( private val actionIds: NotificationActionIds, private val intentProvider: IntentProvider, private val buildMeta: BuildMeta, + private val markAsReadActionFactory: MarkAsReadActionFactory, + private val quickReplyActionFactory: QuickReplyActionFactory, + private val rejectInvitationActionFactory: RejectInvitationActionFactory, + private val acceptInvitationActionFactory: AcceptInvitationActionFactory, ) { companion object { @@ -222,7 +229,6 @@ class NotificationUtils @Inject constructor( threadId: ThreadId?, largeIcon: Bitmap?, lastMessageTimestamp: Long, - senderDisplayNameForReplyCompat: String?, tickerText: String ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) @@ -232,6 +238,7 @@ class NotificationUtils @Inject constructor( true /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ -> buildOpenThreadIntent(roomInfo, threadId) + else -> buildOpenRoomIntent(roomInfo.sessionId, roomInfo.roomId) } @@ -288,43 +295,10 @@ class NotificationUtils @Inject constructor( // 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) } - + addAction(markAsReadActionFactory.create(roomInfo)) // 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) } - } + addAction(quickReplyActionFactory.create(roomInfo, threadId)) } if (openIntent != null) { @@ -366,45 +340,9 @@ class NotificationUtils @Inject constructor( .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) .setColor(accentColor) + .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) + .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) .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 - ) - /* // Build the pending intent for when the notification is clicked val contentIntent = HomeActivity.newIntent( @@ -496,60 +434,6 @@ class NotificationUtils @Inject constructor( ) } - /* - 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 - } - /** * Build the summary notification. */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index ab38ad9fb4..1f308a8d40 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -92,7 +92,6 @@ class RoomGroupMessageCreator @Inject constructor( threadId = lastKnownRoomEvent.threadId, largeIcon = largeBitmap, lastMessageTimestamp, - userDisplayName, tickerText ), meta diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt new file mode 100644 index 0000000000..00ea4e407d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt @@ -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.actions + +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 + val roomId = inviteNotifiableEvent.roomId + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = actionIds.join + intent.data = createIgnoredUri("acceptInvite?${sessionId.value}&${roomId.value}") + 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() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt new file mode 100644 index 0000000000..54159c23cc --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt @@ -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.actions + +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 + val roomId = roomInfo.roomId + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = actionIds.markRoomRead + intent.data = createIgnoredUri("markRead?${sessionId.value}&$${roomId.value}") + 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() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt new file mode 100644 index 0000000000..6139fab6bb --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt @@ -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.actions + +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) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + threadId?.let { + intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it) + } + + 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 + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt new file mode 100644 index 0000000000..8b571d3318 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt @@ -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.actions + +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 + val roomId = inviteNotifiableEvent.roomId + val intent = Intent(context, NotificationBroadcastReceiver::class.java) + intent.action = actionIds.reject + intent.data = createIgnoredUri("rejectInvite?${sessionId.value}&${roomId.value}") + 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() + } +} From a9881341f126c6ab02dcc4c56687fbd71342dfbb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:14:29 +0200 Subject: [PATCH 03/14] Create getDismissRoomPendingIntent() --- .../impl/notifications/NotificationUtils.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index ef4d4c7438..196f7efb05 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -300,26 +300,13 @@ class NotificationUtils @Inject constructor( if (!roomInfo.hasSmartReplyError) { addAction(quickReplyActionFactory.create(roomInfo, threadId)) } - 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) + setDeleteIntent(getDismissRoomPendingIntent(roomInfo)) } .setTicker(tickerText) .build() @@ -487,13 +474,26 @@ class NotificationUtils @Inject constructor( intent.data = createIgnoredUri("deleteSummary?$sessionId") intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) return PendingIntent.getBroadcast( - context.applicationContext, + context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } + private fun getDismissRoomPendingIntent(roomInfo: RoomEventGroupInfo): PendingIntent { + 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) + return PendingIntent.getBroadcast( + context, + clock.epochMillis().toInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + /** * Cancel the foreground notification service. */ From 0677b10de3a61fe877ad963204966de80f6a914f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:17:52 +0200 Subject: [PATCH 04/14] rename package --- .../impl/notifications/NotificationUtils.kt | 19 ++++++++++--------- .../AcceptInvitationActionFactory.kt | 2 +- .../MarkAsReadActionFactory.kt | 2 +- .../QuickReplyActionFactory.kt | 2 +- .../RejectInvitationActionFactory.kt | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{actions => factories}/AcceptInvitationActionFactory.kt (97%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{actions => factories}/MarkAsReadActionFactory.kt (97%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{actions => factories}/QuickReplyActionFactory.kt (98%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{actions => factories}/RejectInvitationActionFactory.kt (97%) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 196f7efb05..83bf1fb414 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -50,10 +50,10 @@ 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.actions.AcceptInvitationActionFactory -import io.element.android.libraries.push.impl.notifications.actions.MarkAsReadActionFactory -import io.element.android.libraries.push.impl.notifications.actions.QuickReplyActionFactory -import io.element.android.libraries.push.impl.notifications.actions.RejectInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.factories.AcceptInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.factories.MarkAsReadActionFactory +import io.element.android.libraries.push.impl.notifications.factories.QuickReplyActionFactory +import io.element.android.libraries.push.impl.notifications.factories.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 @@ -306,7 +306,7 @@ class NotificationUtils @Inject constructor( if (largeIcon != null) { setLargeIcon(largeIcon) } - setDeleteIntent(getDismissRoomPendingIntent(roomInfo)) + setDeleteIntent(getDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) } .setTicker(tickerText) .build() @@ -471,7 +471,7 @@ class NotificationUtils @Inject constructor( private fun getDismissSummaryPendingIntent(sessionId: SessionId): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = actionIds.dismissSummary - intent.data = createIgnoredUri("deleteSummary?$sessionId") + intent.data = createIgnoredUri("deleteSummary?${sessionId.value}") intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) return PendingIntent.getBroadcast( context, @@ -481,11 +481,12 @@ class NotificationUtils @Inject constructor( ) } - private fun getDismissRoomPendingIntent(roomInfo: RoomEventGroupInfo): PendingIntent { + private fun getDismissRoomPendingIntent(sessionId: SessionId, roomId: RoomId): PendingIntent { 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) + intent.data = createIgnoredUri("deleteRoom?${sessionId.value}&${roomId.value}") + intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) return PendingIntent.getBroadcast( context, clock.epochMillis().toInt(), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt similarity index 97% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt index 00ea4e407d..e9be6aafb7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/AcceptInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.actions +package io.element.android.libraries.push.impl.notifications.factories import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt similarity index 97% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt index 54159c23cc..52b01dd8f9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/MarkAsReadActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.actions +package io.element.android.libraries.push.impl.notifications.factories import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt similarity index 98% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt index 6139fab6bb..07e3053b4d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.actions +package io.element.android.libraries.push.impl.notifications.factories import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt similarity index 97% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt index 8b571d3318..b49115b4e5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/actions/RejectInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.actions +package io.element.android.libraries.push.impl.notifications.factories import android.app.PendingIntent import android.content.Context From 4c10c8fdf9cb14b88c8bd8ad69e1c1c82b0204c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:26:01 +0200 Subject: [PATCH 05/14] Create PendingIntentFactory --- .../impl/notifications/NotificationUtils.kt | 93 +++------------- .../factories/PendingIntentFactory.kt | 100 ++++++++++++++++++ .../AcceptInvitationActionFactory.kt | 2 +- .../{ => action}/MarkAsReadActionFactory.kt | 2 +- .../{ => action}/QuickReplyActionFactory.kt | 2 +- .../RejectInvitationActionFactory.kt | 2 +- 6 files changed, 117 insertions(+), 84 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/{ => action}/AcceptInvitationActionFactory.kt (99%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/{ => action}/MarkAsReadActionFactory.kt (99%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/{ => action}/QuickReplyActionFactory.kt (99%) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/{ => action}/RejectInvitationActionFactory.kt (99%) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 83bf1fb414..4d5ae8880f 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -24,9 +24,7 @@ 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 @@ -40,37 +38,31 @@ 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.factories.AcceptInvitationActionFactory -import io.element.android.libraries.push.impl.notifications.factories.MarkAsReadActionFactory -import io.element.android.libraries.push.impl.notifications.factories.QuickReplyActionFactory -import io.element.android.libraries.push.impl.notifications.factories.RejectInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.factories.PendingIntentFactory +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 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, + private val pendingIntentFactory: PendingIntentFactory, private val markAsReadActionFactory: MarkAsReadActionFactory, private val quickReplyActionFactory: QuickReplyActionFactory, private val rejectInvitationActionFactory: RejectInvitationActionFactory, @@ -237,9 +229,9 @@ class NotificationUtils @Inject constructor( threadId != null && true /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ - -> buildOpenThreadIntent(roomInfo, threadId) + -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId) - else -> buildOpenRoomIntent(roomInfo.sessionId, roomInfo.roomId) + else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId) } val smallIcon = R.drawable.ic_notification @@ -306,7 +298,7 @@ class NotificationUtils @Inject constructor( if (largeIcon != null) { setLargeIcon(largeIcon) } - setDeleteIntent(getDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) + setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) } .setTicker(tickerText) .build() @@ -351,7 +343,6 @@ class NotificationUtils @Inject constructor( vectorPreferences.getNotificationRingTone()?.let { setSound(it) } - */ setLights(accentColor, 500, 500) } else { @@ -379,7 +370,7 @@ class NotificationUtils @Inject constructor( .setSmallIcon(smallIcon) .setColor(accentColor) .setAutoCancel(true) - .setContentIntent(buildOpenRoomIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId)) + .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId)) .apply { if (simpleNotifiableEvent.noisy) { // Compat @@ -388,7 +379,6 @@ class NotificationUtils @Inject constructor( vectorPreferences.getNotificationRingTone()?.let { setSound(it) } - */ setLights(accentColor, 500, 500) } else { @@ -399,28 +389,6 @@ class NotificationUtils @Inject constructor( .build() } - private fun buildOpenSessionIntent(sessionId: SessionId): PendingIntent? { - return getPendingIntent(sessionId = sessionId, roomId = null, threadId = null) - } - - private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? { - return getPendingIntent(sessionId = sessionId, roomId = roomId, threadId = null) - } - - private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? { - return getPendingIntent(sessionId = roomInfo.sessionId, roomId = roomInfo.roomId, threadId = threadId) - } - - private fun getPendingIntent(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 - ) - } - /** * Build the summary notification. */ @@ -463,38 +431,11 @@ class NotificationUtils @Inject constructor( priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(buildOpenSessionIntent(sessionId)) - .setDeleteIntent(getDismissSummaryPendingIntent(sessionId)) + .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId)) + .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId)) .build() } - private fun getDismissSummaryPendingIntent(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) - return PendingIntent.getBroadcast( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - - private fun getDismissRoomPendingIntent(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) - intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) - return PendingIntent.getBroadcast( - context, - clock.epochMillis().toInt(), - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - /** * Cancel the foreground notification service. */ @@ -520,14 +461,6 @@ class NotificationUtils @Inject constructor( 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, @@ -540,7 +473,7 @@ class NotificationUtils @Inject constructor( .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_STATUS) .setAutoCancel(true) - .setContentIntent(testPendingIntent) + .setContentIntent(pendingIntentFactory.createTestPendingIntent()) .build() ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt new file mode 100644 index 0000000000..ca662afe17 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt @@ -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) + 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) + intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + 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 + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt index e9be6aafb7..580c36c162 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/AcceptInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.factories +package io.element.android.libraries.push.impl.notifications.factories.action import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt index 52b01dd8f9..4a5e0aa7f3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/MarkAsReadActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.factories +package io.element.android.libraries.push.impl.notifications.factories.action import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt index 07e3053b4d..4f454f2855 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.factories +package io.element.android.libraries.push.impl.notifications.factories.action import android.app.PendingIntent import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt index b49115b4e5..b30297295e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/RejectInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.notifications.factories +package io.element.android.libraries.push.impl.notifications.factories.action import android.app.PendingIntent import android.content.Context From f4e2775d2ad964b7fa05af90f205bcfe534a82b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:37:49 +0200 Subject: [PATCH 06/14] Extract channel management to dedicated class --- .../impl/notifications/NotificationUtils.kt | 157 ++------------- .../channels/NotificationChannels.kt | 179 ++++++++++++++++++ 2 files changed, 190 insertions(+), 146 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 4d5ae8880f..78ab059275 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -20,16 +20,12 @@ 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.content.Context 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 @@ -37,14 +33,12 @@ import androidx.core.app.NotificationManagerCompat 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.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.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.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.factories.PendingIntentFactory import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory @@ -56,9 +50,9 @@ import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber import javax.inject.Inject -@SingleIn(AppScope::class) class NotificationUtils @Inject constructor( @ApplicationContext private val context: Context, + private val notificationChannels: NotificationChannels, // private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider, private val buildMeta: BuildMeta, @@ -81,137 +75,10 @@ class NotificationUtils @Inject constructor( * 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. */ @@ -236,8 +103,8 @@ class NotificationUtils @Inject constructor( 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) + 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. @@ -309,9 +176,8 @@ class NotificationUtils @Inject constructor( ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) 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) + val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) + return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) .setContentText(inviteNotifiableEvent.description) @@ -359,9 +225,8 @@ class NotificationUtils @Inject constructor( val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) 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) + val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) + return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName) .setContentText(simpleNotifiableEvent.description) @@ -401,8 +266,8 @@ class NotificationUtils @Inject constructor( ): 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) + 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) @@ -464,7 +329,7 @@ class NotificationUtils @Inject constructor( notificationManager.notify( "DIAGNOSTIC", 888, - NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) + NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) .setContentTitle(buildMeta.applicationName) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) .setSmallIcon(R.drawable.ic_notification) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt new file mode 100644 index 0000000000..b2c214f7d7 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -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) + } + } +} From ffed09175db53d1c1885241909e8ab541c2fb92f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:49:47 +0200 Subject: [PATCH 07/14] Create NotificationFactory --- .../androidutils/system/SystemUtils.kt | 12 + .../impl/notifications/NotificationFactory.kt | 8 +- .../impl/notifications/NotificationUtils.kt | 305 +----------------- .../notifications/RoomGroupMessageCreator.kt | 5 +- .../SummaryGroupMessageCreator.kt | 7 +- .../factories/NotificationFactory.kt | 295 +++++++++++++++++ 6 files changed, 322 insertions(+), 310 deletions(-) create mode 100755 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index c5bccc97f5..a6c3f93ecf 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.androidutils.system import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Activity +import android.app.NotificationManager import android.content.ActivityNotFoundException import android.content.Context 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()!!.currentInterruptionFilter + + return setting == NotificationManager.INTERRUPTION_FILTER_NONE || + setting == NotificationManager.INTERRUPTION_FILTER_ALARMS +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 1a2d4a852f..4bb49e168f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import io.element.android.libraries.matrix.api.core.RoomId 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.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent @@ -26,8 +27,9 @@ import javax.inject.Inject private typealias ProcessedMessageEvents = List> +// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory class NotificationFactory @Inject constructor( - private val notificationUtils: NotificationUtils, + private val notificationFactory: NotificationFactory, private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { @@ -66,7 +68,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationUtils.buildRoomInvitationNotification(event), + notificationFactory.createRoomInvitationNotification(event), OneShotNotification.Append.Meta( key = event.roomId.value, summaryLine = event.description, @@ -84,7 +86,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationUtils.buildSimpleEventNotification(event), + notificationFactory.createSimpleEventNotification(event), OneShotNotification.Append.Meta( key = event.eventId.value, summaryLine = event.description, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index 78ab059275..29cdff44db 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -20,47 +20,18 @@ package io.element.android.libraries.push.impl.notifications import android.Manifest import android.annotation.SuppressLint -import android.app.Notification -import android.app.NotificationManager import android.content.Context import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.Canvas -import androidx.annotation.DrawableRes import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService -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.channels.NotificationChannels -import io.element.android.libraries.push.impl.notifications.factories.PendingIntentFactory -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 io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import timber.log.Timber import javax.inject.Inject class NotificationUtils @Inject constructor( @ApplicationContext private val context: Context, - private val notificationChannels: NotificationChannels, - // private val vectorPreferences: VectorPreferences, - 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, + private val notificationFactory: NotificationFactory, ) { companion object { @@ -79,228 +50,6 @@ class NotificationUtils @Inject constructor( private val notificationManager = NotificationManagerCompat.from(context) - /** - * Build a notification for a Room. - */ - fun buildMessagesListNotification( - 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 && - true - /** TODO EAx vectorPreferences.areThreadMessagesEnabled() */ - -> 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 buildRoomInvitationNotification( - 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 buildSimpleEventNotification( - 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() - } - - /** - * 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 - 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() - } - /** * Cancel the foreground notification service. */ @@ -329,55 +78,7 @@ class NotificationUtils @Inject constructor( notificationManager.notify( "DIAGNOSTIC", 888, - 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(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(pendingIntentFactory.createTestPendingIntent()) - .build() + notificationFactory.createDiagnosticNotification() ) } - - 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()!!.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 - } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 1f308a8d40..63115c9ccb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -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.SessionId 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.services.toolbox.api.strings.StringProvider import me.gujun.android.span.Span @@ -32,7 +33,7 @@ import javax.inject.Inject class RoomGroupMessageCreator @Inject constructor( private val bitmapLoader: NotificationBitmapLoader, private val stringProvider: StringProvider, - private val notificationUtils: NotificationUtils + private val notificationFactory: NotificationFactory ) { fun createRoomMessage( @@ -76,7 +77,7 @@ class RoomGroupMessageCreator @Inject constructor( shouldBing = events.any { it.noisy } ) return RoomNotification.Message( - notificationUtils.buildMessagesListNotification( + notificationFactory.createMessagesListNotification( style, RoomEventGroupInfo( sessionId = sessionId, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index ed0053e58c..a400c2b7a3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -20,6 +20,7 @@ import android.app.Notification import androidx.core.app.NotificationCompat import io.element.android.libraries.matrix.api.core.SessionId 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 javax.inject.Inject @@ -39,7 +40,7 @@ import javax.inject.Inject */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, - private val notificationUtils: NotificationUtils + private val notificationFactory: NotificationFactory ) { fun createSummaryNotification( @@ -72,7 +73,7 @@ class SummaryGroupMessageCreator @Inject constructor( // TODO get latest event? .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) return if (useCompleteNotificationFormat) { - notificationUtils.buildSummaryListNotification( + notificationFactory.createSummaryListNotification( sessionId, summaryInboxStyle, sumTitle, @@ -165,7 +166,7 @@ class SummaryGroupMessageCreator @Inject constructor( messageStr } } - return notificationUtils.buildSummaryListNotification( + return notificationFactory.createSummaryListNotification( sessionId = sessionId, style = null, compatSummary = privacyTitle, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt new file mode 100755 index 0000000000..1bd2e313a8 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -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(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(pendingIntentFactory.createTestPendingIntent()) + .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 + } +} From 5f3d33ca72287204f8adcd322445408ecca0ad22 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 17:52:39 +0200 Subject: [PATCH 08/14] Improve temporary message displayed in notifications --- .../push/impl/notifications/NotifiableEventResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 5ae39e1102..3c82496626 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -89,7 +89,7 @@ private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId timestamp = System.currentTimeMillis(), senderName = null, senderId = null, - body = "$eventId in $roomId", + body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", imageUriString = null, threadId = null, roomName = null, From 8a91d31c244b7dac13f88daad89ee83a96be72ca Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 18:10:55 +0200 Subject: [PATCH 09/14] Fix issue with pendingintent data. (Simplier to use String that Serializable) --- .../notifications/factories/PendingIntentFactory.kt | 10 +++++----- .../factories/action/AcceptInvitationActionFactory.kt | 6 +++--- .../factories/action/MarkAsReadActionFactory.kt | 6 +++--- .../factories/action/QuickReplyActionFactory.kt | 8 ++++---- .../factories/action/RejectInvitationActionFactory.kt | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt index ca662afe17..4979eb3ddb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt @@ -63,8 +63,8 @@ class PendingIntentFactory @Inject constructor( 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) + intent.data = createIgnoredUri("deleteSummary/${sessionId.value}") + intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) return PendingIntent.getBroadcast( context, 0, @@ -76,9 +76,9 @@ class PendingIntentFactory @Inject constructor( 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) - intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + 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(), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt index 580c36c162..06ef22247e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/AcceptInvitationActionFactory.kt @@ -38,11 +38,11 @@ class AcceptInvitationActionFactory @Inject constructor( ) { // offer to type a quick accept button fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action { - val sessionId = inviteNotifiableEvent.sessionId - val roomId = inviteNotifiableEvent.roomId + 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.value}&${roomId.value}") + intent.data = createIgnoredUri("acceptInvite/$sessionId/$roomId") intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val pendingIntent = PendingIntent.getBroadcast( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt index 4a5e0aa7f3..0dcf4bb326 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/MarkAsReadActionFactory.kt @@ -39,11 +39,11 @@ class MarkAsReadActionFactory @Inject constructor( ) { fun create(roomInfo: RoomEventGroupInfo): NotificationCompat.Action? { if (!NotificationConfig.supportMarkAsReadAction) return null - val sessionId = roomInfo.sessionId - val roomId = roomInfo.roomId + 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.value}&$${roomId.value}") + intent.data = createIgnoredUri("markRead/$sessionId/$roomId") intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val pendingIntent = PendingIntent.getBroadcast( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt index 4f454f2855..a42ba3c86e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt @@ -78,11 +78,11 @@ class QuickReplyActionFactory @Inject constructor( 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) - intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + 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) + intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it.value) } PendingIntent.getBroadcast( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt index b30297295e..75e1cdaf99 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/RejectInvitationActionFactory.kt @@ -37,11 +37,11 @@ class RejectInvitationActionFactory @Inject constructor( private val clock: SystemClock, ) { fun create(inviteNotifiableEvent: InviteNotifiableEvent): NotificationCompat.Action? { - val sessionId = inviteNotifiableEvent.sessionId - val roomId = inviteNotifiableEvent.roomId + 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.value}&${roomId.value}") + intent.data = createIgnoredUri("rejectInvite/$sessionId/$roomId") intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) val pendingIntent = PendingIntent.getBroadcast( From d7cd24b3d065c5ff077c4b48467f8e7753ff8d53 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 18:19:59 +0200 Subject: [PATCH 10/14] small cleanup --- .../push/impl/notifications/factories/NotificationFactory.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt index 1bd2e313a8..5795ea5f5f 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -274,7 +274,7 @@ class NotificationFactory @Inject constructor( .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)) + .setLargeIcon(getBitmap(R.drawable.element_logo_green)) .setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_STATUS) @@ -283,7 +283,7 @@ class NotificationFactory @Inject constructor( .build() } - private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { + 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) From e92fe45e3c8f0b93c69e3a2f2b4fd311243c598e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 18:31:35 +0200 Subject: [PATCH 11/14] Improve rendering of notifications --- .../push/impl/notifications/RoomGroupMessageCreator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 63115c9ccb..00222728bf 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -44,7 +44,7 @@ class RoomGroupMessageCreator @Inject constructor( userAvatarUrl: String? ): RoomNotification.Message { 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 style = NotificationCompat.MessagingStyle( Person.Builder() From ebbd1372a980d2dddc9254c1db3ccec5ee2c7f5d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 14 Apr 2023 18:34:49 +0200 Subject: [PATCH 12/14] More cleanup --- .../notifications/NotificationDisplayer.kt | 33 ++++++++ .../impl/notifications/NotificationUtils.kt | 84 ------------------- 2 files changed, 33 insertions(+), 84 deletions(-) delete mode 100755 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index c8331ed8cd..2cb01ba2f7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl.notifications import android.Manifest +import android.annotation.SuppressLint import android.app.Notification import android.content.Context import android.content.pm.PackageManager @@ -51,4 +52,36 @@ class NotificationDisplayer @Inject constructor( 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 + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt deleted file mode 100755 index 29cdff44db..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ /dev/null @@ -1,84 +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.content.Context -import android.content.pm.PackageManager -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationManagerCompat -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory -import timber.log.Timber -import javax.inject.Inject - -class NotificationUtils @Inject constructor( - @ApplicationContext private val context: Context, - private val notificationFactory: NotificationFactory, -) { - - 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 - } - - private val notificationManager = NotificationManagerCompat.from(context) - - /** - * 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 - } - notificationManager.notify( - "DIAGNOSTIC", - 888, - notificationFactory.createDiagnosticNotification() - ) - } -} From e8d1490a92a534f2426f78a651c539025e2f5e47 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Apr 2023 09:42:59 +0200 Subject: [PATCH 13/14] Small quality fixes --- .../libraries/push/impl/{Config.kt => NotificationConfig.kt} | 0 .../push/impl/notifications/channels/NotificationChannels.kt | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/{Config.kt => NotificationConfig.kt} (100%) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/NotificationConfig.kt similarity index 100% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/Config.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/NotificationConfig.kt diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index b2c214f7d7..fc4838e9a3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -33,7 +33,7 @@ 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 + * on devices >= android O, we need to define a channel for each notifications. */ @SingleIn(AppScope::class) class NotificationChannels @Inject constructor( From ae7137bd1da82bd016e22f33d438dd7276f779f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Apr 2023 09:51:52 +0200 Subject: [PATCH 14/14] Fix tests. --- .../impl/notifications/NotificationFactoryTest.kt | 10 +++++----- ...nUtils.kt => FakeAndroidNotificationFactory.kt} | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) rename libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/{FakeNotificationUtils.kt => FakeAndroidNotificationFactory.kt} (66%) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt index 606923eb1f..7cc9479ee9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt @@ -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.A_ROOM_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.FakeSummaryGroupMessageCreator 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 { - private val notificationUtils = FakeNotificationUtils() + private val androidNotificationFactory = FakeAndroidNotificationFactory() private val roomGroupMessageCreator = FakeRoomGroupMessageCreator() private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator() private val notificationFactory = NotificationFactory( - notificationUtils.instance, + androidNotificationFactory.instance, roomGroupMessageCreator.instance, summaryGroupMessageCreator.instance ) @Test 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 result = roomInvitation.toNotifications() @@ -85,7 +85,7 @@ class NotificationFactoryTest { @Test 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 result = roomInvitation.toNotifications() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationUtils.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt similarity index 66% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationUtils.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt index 046b2edd87..c046e1253f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationUtils.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt @@ -17,24 +17,24 @@ package io.element.android.libraries.push.impl.notifications.fake 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.SimpleNotifiableEvent import io.mockk.every import io.mockk.mockk -class FakeNotificationUtils { - val instance = mockk() +class FakeAndroidNotificationFactory { + val instance = mockk() - fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification { + fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification { val mockNotification = mockk() - every { instance.buildRoomInvitationNotification(event) } returns mockNotification + every { instance.createRoomInvitationNotification(event) } returns mockNotification return mockNotification } - fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification { + fun givenCreateSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification { val mockNotification = mockk() - every { instance.buildSimpleEventNotification(event) } returns mockNotification + every { instance.createSimpleEventNotification(event) } returns mockNotification return mockNotification } }