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) + } + } +}