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
12 changed files with 95 additions and 98 deletions

View File

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

View File

@@ -16,6 +16,9 @@
package io.element.android.appconfig
import android.graphics.Color
import androidx.annotation.ColorInt
object NotificationConfig {
// TODO EAx Implement and set to true at some point
const val SUPPORT_MARK_AS_READ_ACTION = false
@@ -25,4 +28,7 @@ object NotificationConfig {
// TODO EAx Implement and set to true at some point
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(fixFailed = true),
aValidNotificationSettingsState(fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(permissionGranted = false)),
aValidNotificationSettingsState(appNotificationEnabled = false),
)
}

View File

@@ -16,8 +16,6 @@
package io.element.android.libraries.push.impl.notifications.channels
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
@@ -26,8 +24,8 @@ import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
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.ApplicationContext
import io.element.android.libraries.di.SingleIn
@@ -38,14 +36,9 @@ import javax.inject.Inject
/* ==========================================================================================
* 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 NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
// 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 CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V3"
internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID"
/**
@@ -96,7 +89,7 @@ class DefaultNotificationChannels @Inject constructor(
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
// + currentTimeMillis).
@@ -110,76 +103,62 @@ class DefaultNotificationChannels @Inject constructor(
}
}
// 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.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
* intrude.
*/
notificationManager.createNotificationChannel(
NotificationChannel(
NotificationChannelCompat.Builder(
NOISY_NOTIFICATION_CHANNEL_ID,
stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" },
NotificationManager.IMPORTANCE_DEFAULT
NotificationManagerCompat.IMPORTANCE_DEFAULT
)
.apply {
description = stringProvider.getString(R.string.notification_channel_noisy)
enableVibration(true)
enableLights(true)
lightColor = accentColor
}
.setName(stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" })
.setDescription(stringProvider.getString(R.string.notification_channel_noisy))
.setVibrationEnabled(true)
.setLightsEnabled(true)
.setLightColor(accentColor)
.build()
)
/**
* Low notification importance: shows everywhere, but is not intrusive.
*/
notificationManager.createNotificationChannel(
NotificationChannel(
NotificationChannelCompat.Builder(
SILENT_NOTIFICATION_CHANNEL_ID,
stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" },
NotificationManager.IMPORTANCE_LOW
NotificationManagerCompat.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)
}
.setName(stringProvider.getString(R.string.notification_channel_silent).ifEmpty { "Silent notifications" })
.setDescription(stringProvider.getString(R.string.notification_channel_silent))
.setSound(null, null)
.setLightsEnabled(true)
.setLightColor(accentColor)
.build()
)
// Register a channel for incoming and in progress call notifications with no ringing
notificationManager.createNotificationChannel(
NotificationChannel(
CALL_NOTIFICATION_CHANNEL_ID_V3,
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
NotificationManager.IMPORTANCE_HIGH
NotificationChannelCompat.Builder(
CALL_NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.apply {
description = stringProvider.getString(R.string.notification_channel_call)
enableVibration(true)
enableLights(true)
lightColor = accentColor
}
.setName(stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" })
.setDescription(stringProvider.getString(R.string.notification_channel_call))
.setVibrationEnabled(true)
.setLightsEnabled(true)
.setLightColor(accentColor)
.build()
)
// 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 {
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 {

View File

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

View File

@@ -96,17 +96,19 @@ class DefaultPushHandler @Inject constructor(
Timber.w("Unable to get a session")
return
}
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (notifiableEvent) {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
when (notifiableEvent) {
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
else -> {
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
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) {
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
import android.app.NotificationChannel
import android.os.Build
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
@@ -43,7 +42,6 @@ class NotificationChannelsTest {
createNotificationChannels(notificationManager = notificationManager)
verify { notificationManager.createNotificationChannel(any<NotificationChannelCompat>()) }
verify { notificationManager.createNotificationChannel(any<NotificationChannel>()) }
verify { notificationManager.deleteNotificationChannel(any<String>()) }
}
@@ -55,7 +53,7 @@ class NotificationChannelsTest {
assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID)
val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false)
assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID_V3)
assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID)
}
@Test

View File

@@ -118,7 +118,7 @@ class DefaultPushHandlerTest {
incrementPushCounterResult.assertions()
.isCalledOnce()
notifiableEventResult.assertions()
.isNeverCalled()
.isCalledOnce()
onNotifiableEventReceived.assertions()
.isNeverCalled()
}
@@ -277,6 +277,34 @@ class DefaultPushHandlerTest {
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
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() =
runTest {