Merge pull request #3053 from element-hq/feature/bma/callSettings

Alert for incoming call even if notifications are disabled - WAITING FOR FINAL PRODUCT DECISION
This commit is contained in:
Benoit Marty 2024-06-28 14:21:23 +02:00 committed by GitHub
commit 58cdd37a57
12 changed files with 95 additions and 98 deletions

View file

@ -28,6 +28,7 @@ anvil {
} }
dependencies { dependencies {
implementation(libs.androidx.annotationjvm)
implementation(libs.dagger) implementation(libs.dagger)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)

View file

@ -16,6 +16,9 @@
package io.element.android.appconfig package io.element.android.appconfig
import android.graphics.Color
import androidx.annotation.ColorInt
object NotificationConfig { object NotificationConfig {
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val SUPPORT_MARK_AS_READ_ACTION = false const val SUPPORT_MARK_AS_READ_ACTION = false
@ -25,4 +28,7 @@ object NotificationConfig {
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val SUPPORT_QUICK_REPLY_ACTION = false const val SUPPORT_QUICK_REPLY_ACTION = false
@ColorInt
val NOTIFICATION_ACCENT_COLOR: Int = Color.parseColor("#FF0DBD8B")
} }

1
changelog.d/3053.misc Normal file
View file

@ -0,0 +1 @@
Alert for incoming call even if notifications are disabled

View file

@ -42,6 +42,7 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider<Notifica
aInvalidNotificationSettingsState(), aInvalidNotificationSettingsState(),
aInvalidNotificationSettingsState(fixFailed = true), aInvalidNotificationSettingsState(fixFailed = true),
aValidNotificationSettingsState(fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(permissionGranted = false)), aValidNotificationSettingsState(fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(permissionGranted = false)),
aValidNotificationSettingsState(appNotificationEnabled = false),
) )
} }

View file

@ -16,8 +16,6 @@
package io.element.android.libraries.push.impl.notifications.channels package io.element.android.libraries.push.impl.notifications.channels
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioManager import android.media.AudioManager
@ -26,8 +24,8 @@ import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.ChecksSdkIntAtLeast
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.NotificationConfig
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
@ -38,14 +36,9 @@ import javax.inject.Inject
/* ========================================================================================== /* ==========================================================================================
* IDs for channels * IDs for channels
* ========================================================================================== */ * ========================================================================================== */
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
internal const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V3"
// Legacy channel
private const val CALL_NOTIFICATION_CHANNEL_ID_V2 = "CALL_NOTIFICATION_CHANNEL_ID_V2"
internal const val CALL_NOTIFICATION_CHANNEL_ID_V3 = "CALL_NOTIFICATION_CHANNEL_ID_V3"
internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID" internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID"
/** /**
@ -96,7 +89,7 @@ class DefaultNotificationChannels @Inject constructor(
return return
} }
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR
// Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE // Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE
// + currentTimeMillis). // + currentTimeMillis).
@ -110,76 +103,62 @@ class DefaultNotificationChannels @Inject constructor(
} }
} }
// Migration - Remove deprecated channels // Migration - Remove deprecated channels
for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) { for (channelId in listOf(
"DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID",
"CALL_NOTIFICATION_CHANNEL_ID",
"CALL_NOTIFICATION_CHANNEL_ID_V2",
"LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID",
)) {
notificationManager.getNotificationChannel(channelId)?.let { notificationManager.getNotificationChannel(channelId)?.let {
notificationManager.deleteNotificationChannel(channelId) notificationManager.deleteNotificationChannel(channelId)
} }
} }
// Migration - Create new call channel
notificationManager.deleteNotificationChannel(CALL_NOTIFICATION_CHANNEL_ID_V2)
/** /**
* Default notification importance: shows everywhere, makes noise, but does not visually * Default notification importance: shows everywhere, makes noise, but does not visually
* intrude. * intrude.
*/ */
notificationManager.createNotificationChannel( notificationManager.createNotificationChannel(
NotificationChannel( NotificationChannelCompat.Builder(
NOISY_NOTIFICATION_CHANNEL_ID, NOISY_NOTIFICATION_CHANNEL_ID,
stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" }, NotificationManagerCompat.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT
) )
.apply { .setName(stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" })
description = stringProvider.getString(R.string.notification_channel_noisy) .setDescription(stringProvider.getString(R.string.notification_channel_noisy))
enableVibration(true) .setVibrationEnabled(true)
enableLights(true) .setLightsEnabled(true)
lightColor = accentColor .setLightColor(accentColor)
} .build()
) )
/** /**
* Low notification importance: shows everywhere, but is not intrusive. * Low notification importance: shows everywhere, but is not intrusive.
*/ */
notificationManager.createNotificationChannel( notificationManager.createNotificationChannel(
NotificationChannel( NotificationChannelCompat.Builder(
SILENT_NOTIFICATION_CHANNEL_ID, SILENT_NOTIFICATION_CHANNEL_ID,
stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" }, NotificationManagerCompat.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW
) )
.apply { .setName(stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" })
description = stringProvider.getString(R.string.notification_channel_silent) .setDescription(stringProvider.getString(R.string.notification_channel_silent))
setSound(null, null) .setSound(null, null)
enableLights(true) .setLightsEnabled(true)
lightColor = accentColor .setLightColor(accentColor)
} .build()
)
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)
}
) )
// Register a channel for incoming and in progress call notifications with no ringing // Register a channel for incoming and in progress call notifications with no ringing
notificationManager.createNotificationChannel( notificationManager.createNotificationChannel(
NotificationChannel( NotificationChannelCompat.Builder(
CALL_NOTIFICATION_CHANNEL_ID_V3, CALL_NOTIFICATION_CHANNEL_ID,
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" }, NotificationManagerCompat.IMPORTANCE_HIGH
NotificationManager.IMPORTANCE_HIGH
) )
.apply { .setName(stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" })
description = stringProvider.getString(R.string.notification_channel_call) .setDescription(stringProvider.getString(R.string.notification_channel_call))
enableVibration(true) .setVibrationEnabled(true)
enableLights(true) .setLightsEnabled(true)
lightColor = accentColor .setLightColor(accentColor)
} .build()
) )
// Register a channel for incoming call notifications which will ring the device when received // Register a channel for incoming call notifications which will ring the device when received
@ -207,7 +186,7 @@ class DefaultNotificationChannels @Inject constructor(
} }
override fun getChannelForIncomingCall(ring: Boolean): String { override fun getChannelForIncomingCall(ring: Boolean): String {
return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3 return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID
} }
override fun getChannelIdForMessage(noisy: Boolean): String { override fun getChannelIdForMessage(noisy: Boolean): String {

View file

@ -24,7 +24,6 @@ import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person import androidx.core.app.Person
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import coil.ImageLoader import coil.ImageLoader
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
@ -107,6 +106,8 @@ class DefaultNotificationCreator @Inject constructor(
private val acceptInvitationActionFactory: AcceptInvitationActionFactory, private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
private val rejectInvitationActionFactory: RejectInvitationActionFactory private val rejectInvitationActionFactory: RejectInvitationActionFactory
) : NotificationCreator { ) : NotificationCreator {
private val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR
/** /**
* Create a notification for a Room. * Create a notification for a Room.
*/ */
@ -121,7 +122,6 @@ class DefaultNotificationCreator @Inject constructor(
imageLoader: ImageLoader, imageLoader: ImageLoader,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val openIntent = when { val openIntent = when {
threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId) threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo, threadId)
@ -228,7 +228,6 @@ class DefaultNotificationCreator @Inject constructor(
override fun createRoomInvitationNotification( override fun createRoomInvitationNotification(
inviteNotifiableEvent: InviteNotifiableEvent inviteNotifiableEvent: InviteNotifiableEvent
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = CommonDrawables.ic_notification_small val smallIcon = CommonDrawables.ic_notification_small
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
@ -273,7 +272,6 @@ class DefaultNotificationCreator @Inject constructor(
override fun createSimpleEventNotification( override fun createSimpleEventNotification(
simpleNotifiableEvent: SimpleNotifiableEvent, simpleNotifiableEvent: SimpleNotifiableEvent,
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = CommonDrawables.ic_notification_small val smallIcon = CommonDrawables.ic_notification_small
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
@ -307,7 +305,6 @@ class DefaultNotificationCreator @Inject constructor(
override fun createFallbackNotification( override fun createFallbackNotification(
fallbackNotifiableEvent: FallbackNotifiableEvent, fallbackNotifiableEvent: FallbackNotifiableEvent,
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = CommonDrawables.ic_notification_small val smallIcon = CommonDrawables.ic_notification_small
val channelId = notificationChannels.getChannelIdForMessage(false) val channelId = notificationChannels.getChannelIdForMessage(false)
@ -344,7 +341,6 @@ class DefaultNotificationCreator @Inject constructor(
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long lastMessageTimestamp: Long
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = CommonDrawables.ic_notification_small val smallIcon = CommonDrawables.ic_notification_small
val channelId = notificationChannels.getChannelIdForMessage(noisy) val channelId = notificationChannels.getChannelIdForMessage(noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
@ -384,7 +380,7 @@ class DefaultNotificationCreator @Inject constructor(
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
.setSmallIcon(CommonDrawables.ic_notification_small) .setSmallIcon(CommonDrawables.ic_notification_small)
.setLargeIcon(getBitmap(R.drawable.element_logo_green)) .setLargeIcon(getBitmap(R.drawable.element_logo_green))
.setColor(ContextCompat.getColor(context, R.color.notification_accent_color)) .setColor(accentColor)
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_STATUS) .setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true) .setAutoCancel(true)

View file

@ -96,17 +96,19 @@ class DefaultPushHandler @Inject constructor(
Timber.w("Unable to get a session") Timber.w("Unable to get a session")
return return
} }
val userPushStore = userPushStoreFactory.getOrCreate(userId) val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first() when (notifiableEvent) {
if (areNotificationsEnabled) { null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
when (notifiableEvent) { else -> {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data") val userPushStore = userPushStoreFactory.getOrCreate(userId)
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent) val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
}
} }
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<resources>
<color name="notification_accent_color">#368BD6</color>
</resources>

View file

@ -16,7 +16,6 @@
package io.element.android.libraries.push.impl.notifications.channels package io.element.android.libraries.push.impl.notifications.channels
import android.app.NotificationChannel
import android.os.Build import android.os.Build
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -43,7 +42,6 @@ class NotificationChannelsTest {
createNotificationChannels(notificationManager = notificationManager) createNotificationChannels(notificationManager = notificationManager)
verify { notificationManager.createNotificationChannel(any<NotificationChannelCompat>()) } verify { notificationManager.createNotificationChannel(any<NotificationChannelCompat>()) }
verify { notificationManager.createNotificationChannel(any<NotificationChannel>()) }
verify { notificationManager.deleteNotificationChannel(any<String>()) } verify { notificationManager.deleteNotificationChannel(any<String>()) }
} }
@ -55,7 +53,7 @@ class NotificationChannelsTest {
assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID) assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID)
val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false) val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false)
assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID_V3) assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID)
} }
@Test @Test

View file

@ -118,7 +118,7 @@ class DefaultPushHandlerTest {
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isNeverCalled() .isCalledOnce()
onNotifiableEventReceived.assertions() onNotifiableEventReceived.assertions()
.isNeverCalled() .isNeverCalled()
} }
@ -277,6 +277,34 @@ class DefaultPushHandlerTest {
onNotifiableEventReceived.assertions().isCalledOnce() onNotifiableEventReceived.assertions().isCalledOnce()
} }
@Test
fun `when notify call PushData is received, the incoming call will be treated as a normal notification even if notification are disabled`() = runTest {
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val handleIncomingCallLambda = lambdaRecorder<CallType.RoomCall, EventId, UserId, String?, String?, String?, String, Unit> { _, _, _, _, _, _, _ -> }
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint,
onNotifiableEventReceived = onNotifiableEventReceived,
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent() },
incrementPushCounterResult = {},
userPushStore = FakeUserPushStore().apply {
setNotificationEnabledForDevice(false)
},
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
)
defaultPushHandler.handle(aPushData)
handleIncomingCallLambda.assertions().isCalledOnce()
onNotifiableEventReceived.assertions().isNeverCalled()
}
@Test @Test
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() = fun `when diagnostic PushData is received, the diagnostic push handler is informed `() =
runTest { runTest {

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b97743618b94a6f0e9f3e9d1e776f818deafb8e1bfb9671e85497359b2d45a95
size 15210

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea1f406cdc673d242f6a8920da2f15316961f9765ddb43e189ef1c057417658a
size 14762