diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
index bbd9f62689..64d75c6aa9 100644
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
+++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt
@@ -17,5 +17,13 @@
package io.element.android.appconfig
object ElementCallConfig {
+ /**
+ * The default base URL for the Element Call service.
+ */
const val DEFAULT_BASE_URL = "https://call.element.io"
+
+ /**
+ * The default duration of a ringing call in seconds before it's automatically dismissed.
+ */
+ const val RINGING_CALL_DURATION_SECONDS = 15
}
diff --git a/changelog.d/2894.feature b/changelog.d/2894.feature
new file mode 100644
index 0000000000..6c9067ebed
--- /dev/null
+++ b/changelog.d/2894.feature
@@ -0,0 +1 @@
+Ringing call notifications and full screen ringing screen for DMs when the device is locked.
diff --git a/features/call/api/build.gradle.kts b/features/call/api/build.gradle.kts
new file mode 100644
index 0000000000..100960d544
--- /dev/null
+++ b/features/call/api/build.gradle.kts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2024 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.
+ */
+
+plugins {
+ id("io.element.android-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.call.api"
+}
+
+dependencies {
+ implementation(projects.anvilannotations)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
similarity index 92%
rename from features/call/src/main/kotlin/io/element/android/features/call/CallType.kt
rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
index a3615671b6..4ce3a35fb6 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/CallType.kt
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 New Vector Ltd
+ * Copyright (c) 2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call
+package io.element.android.features.call.api
import android.os.Parcelable
import io.element.android.libraries.architecture.NodeInputs
diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt
new file mode 100644
index 0000000000..5f4a5c3a65
--- /dev/null
+++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 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.features.call.api
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+
+/**
+ * Entry point for the call feature.
+ */
+interface ElementCallEntryPoint {
+ /**
+ * Start a call of the given type.
+ * @param callType The type of call to start.
+ */
+ fun startCall(callType: CallType)
+
+ /**
+ * Handle an incoming call.
+ * @param callType The type of call.
+ * @param eventId The event id of the event that started the call.
+ * @param senderId The user id of the sender of the event that started the call.
+ * @param roomName The name of the room the call is in.
+ * @param senderName The name of the sender of the event that started the call.
+ * @param avatarUrl The avatar url of the room or DM.
+ * @param timestamp The timestamp of the event that started the call.
+ * @param notificationChannelId The id of the notification channel to use for the call notification.
+ */
+ fun handleIncomingCall(
+ callType: CallType.RoomCall,
+ eventId: EventId,
+ senderId: UserId,
+ roomName: String?,
+ senderName: String?,
+ avatarUrl: String?,
+ timestamp: Long,
+ notificationChannelId: String,
+ )
+}
diff --git a/features/call/build.gradle.kts b/features/call/impl/build.gradle.kts
similarity index 78%
rename from features/call/build.gradle.kts
rename to features/call/impl/build.gradle.kts
index 7ff1510cef..72ccdc089b 100644
--- a/features/call/build.gradle.kts
+++ b/features/call/impl/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 New Vector Ltd
+ * Copyright (c) 2024 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.
@@ -23,11 +23,15 @@ plugins {
}
android {
- namespace = "io.element.android.features.call"
+ namespace = "io.element.android.features.call.impl"
buildFeatures {
buildConfig = true
}
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ }
}
anvil {
@@ -41,12 +45,17 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.impl)
+ implementation(projects.libraries.matrixui)
implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api)
+ implementation(projects.libraries.push.api)
+ implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api)
implementation(libs.androidx.webkit)
+ implementation(libs.coil.compose)
implementation(libs.serialization.json)
+ api(projects.features.call.api)
ksp(libs.showkase.processor)
testImplementation(libs.coroutines.test)
@@ -54,9 +63,12 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
+ testImplementation(libs.test.mockk)
+ testImplementation(projects.features.call.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils)
}
diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml
similarity index 75%
rename from features/call/src/main/AndroidManifest.xml
rename to features/call/impl/src/main/AndroidManifest.xml
index 532d5bc40c..354ea7533d 100644
--- a/features/call/src/main/AndroidManifest.xml
+++ b/features/call/impl/src/main/AndroidManifest.xml
@@ -23,11 +23,17 @@
android:name="android.hardware.microphone"
android:required="false" />
+
+
+
+
+
+
+
+
+
+ android:exported="false"
+ android:foregroundServiceType="phoneCall" />
+
+
+
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt
new file mode 100644
index 0000000000..8350e01e8d
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl
+
+import android.content.Context
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.features.call.impl.utils.ActiveCallManager
+import io.element.android.features.call.impl.utils.IntentProvider
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultElementCallEntryPoint @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val activeCallManager: ActiveCallManager,
+) : ElementCallEntryPoint {
+ companion object {
+ const val EXTRA_CALL_TYPE = "EXTRA_CALL_TYPE"
+ const val REQUEST_CODE = 2255
+ }
+
+ override fun startCall(callType: CallType) {
+ context.startActivity(IntentProvider.createIntent(context, callType))
+ }
+
+ override fun handleIncomingCall(
+ callType: CallType.RoomCall,
+ eventId: EventId,
+ senderId: UserId,
+ roomName: String?,
+ senderName: String?,
+ avatarUrl: String?,
+ timestamp: Long,
+ notificationChannelId: String,
+ ) {
+ val incomingCallNotificationData = CallNotificationData(
+ sessionId = callType.sessionId,
+ roomId = callType.roomId,
+ eventId = eventId,
+ senderId = senderId,
+ roomName = roomName,
+ senderName = senderName,
+ avatarUrl = avatarUrl,
+ timestamp = timestamp,
+ notificationChannelId = notificationChannelId,
+ )
+ activeCallManager.registerIncomingCall(notificationData = incomingCallNotificationData)
+ }
+}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
similarity index 96%
rename from features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
index 0d9be99cf7..a75132abae 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/data/WidgetMessage.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.data
+package io.element.android.features.call.impl.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
similarity index 67%
rename from features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
index acbf3dba1f..88fea81149 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/di/CallBindings.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/di/CallBindings.kt
@@ -14,13 +14,17 @@
* limitations under the License.
*/
-package io.element.android.features.call.di
+package io.element.android.features.call.impl.di
import com.squareup.anvil.annotations.ContributesTo
-import io.element.android.features.call.ui.ElementCallActivity
+import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver
+import io.element.android.features.call.impl.ui.ElementCallActivity
+import io.element.android.features.call.impl.ui.IncomingCallActivity
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface CallBindings {
fun inject(callActivity: ElementCallActivity)
+ fun inject(callActivity: IncomingCallActivity)
+ fun inject(declineCallBroadcastReceiver: DeclineCallBroadcastReceiver)
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt
new file mode 100644
index 0000000000..9f49c6e869
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.notifications
+
+import android.os.Parcelable
+import io.element.android.libraries.matrix.api.core.EventId
+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.UserId
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class CallNotificationData(
+ val sessionId: SessionId,
+ val roomId: RoomId,
+ val eventId: EventId,
+ val senderId: UserId,
+ val roomName: String?,
+ val senderName: String?,
+ val avatarUrl: String?,
+ val notificationChannelId: String,
+ val timestamp: Long,
+) : Parcelable
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
new file mode 100644
index 0000000000..cd4c3692d2
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.notifications
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.media.AudioManager
+import android.media.RingtoneManager
+import androidx.core.app.NotificationCompat
+import androidx.core.app.PendingIntentCompat
+import androidx.core.app.Person
+import io.element.android.appconfig.ElementCallConfig
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.receivers.DeclineCallBroadcastReceiver
+import io.element.android.features.call.impl.ui.IncomingCallActivity
+import io.element.android.features.call.impl.utils.IntentProvider
+import io.element.android.libraries.designsystem.utils.CommonDrawables
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.matrix.api.MatrixClientProvider
+import io.element.android.libraries.matrix.api.core.EventId
+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.UserId
+import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Creates a notification for a ringing call.
+ */
+class RingingCallNotificationCreator @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val matrixClientProvider: MatrixClientProvider,
+ private val imageLoaderHolder: ImageLoaderHolder,
+ private val notificationBitmapLoader: NotificationBitmapLoader,
+) {
+ companion object {
+ /**
+ * Request code for the decline action.
+ */
+ const val DECLINE_REQUEST_CODE = 1
+
+ /**
+ * Request code for the full screen intent.
+ */
+ const val FULL_SCREEN_INTENT_REQUEST_CODE = 2
+ }
+
+ suspend fun createNotification(
+ sessionId: SessionId,
+ roomId: RoomId,
+ eventId: EventId,
+ senderId: UserId,
+ roomName: String?,
+ senderDisplayName: String,
+ roomAvatarUrl: String?,
+ notificationChannelId: String,
+ timestamp: Long,
+ ): Notification? {
+ val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
+ val imageLoader = imageLoaderHolder.get(matrixClient)
+ val largeIcon = notificationBitmapLoader.getUserIcon(roomAvatarUrl, imageLoader)
+
+ val caller = Person.Builder()
+ .setName(senderDisplayName)
+ .setIcon(largeIcon)
+ .setImportant(true)
+ .build()
+
+ val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId))
+
+ val declineIntent = PendingIntentCompat.getBroadcast(
+ context,
+ DECLINE_REQUEST_CODE,
+ Intent(context, DeclineCallBroadcastReceiver::class.java),
+ PendingIntent.FLAG_CANCEL_CURRENT,
+ false,
+ )!!
+
+ val fullScreenIntent = PendingIntentCompat.getActivity(
+ context,
+ FULL_SCREEN_INTENT_REQUEST_CODE,
+ Intent(context, IncomingCallActivity::class.java).apply {
+ putExtra(
+ IncomingCallActivity.EXTRA_NOTIFICATION_DATA,
+ CallNotificationData(sessionId, roomId, eventId, senderId, roomName, senderDisplayName, roomAvatarUrl, notificationChannelId, timestamp)
+ )
+ },
+ PendingIntent.FLAG_CANCEL_CURRENT,
+ false
+ )
+
+ val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
+ return NotificationCompat.Builder(context, notificationChannelId)
+ .setSmallIcon(CommonDrawables.ic_notification_small)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, declineIntent, answerIntent).setIsVideo(true))
+ .addPerson(caller)
+ .setAutoCancel(true)
+ .setWhen(timestamp)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setSound(ringtoneUri, AudioManager.STREAM_RING)
+ .setTimeoutAfter(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds.inWholeMilliseconds)
+ .setContentIntent(answerIntent)
+ .setDeleteIntent(declineIntent)
+ .setFullScreenIntent(fullScreenIntent, true)
+ .build()
+ .apply {
+ flags = flags.or(Notification.FLAG_INSISTENT)
+ }
+ }
+}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
new file mode 100644
index 0000000000..27b46db5fe
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import io.element.android.features.call.impl.di.CallBindings
+import io.element.android.features.call.impl.utils.ActiveCallManager
+import io.element.android.libraries.architecture.bindings
+import javax.inject.Inject
+
+/**
+ * Broadcast receiver to decline the incoming call.
+ */
+class DeclineCallBroadcastReceiver : BroadcastReceiver() {
+ @Inject
+ lateinit var activeCallManager: ActiveCallManager
+ override fun onReceive(context: Context, intent: Intent?) {
+ context.bindings().inject(this)
+ activeCallManager.hungUpCall()
+ }
+}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
similarity index 69%
rename from features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
index 10168eba2f..29f7f92139 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/CallForegroundService.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 New Vector Ltd
+ * Copyright (c) 2024 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.
@@ -14,30 +14,36 @@
* limitations under the License.
*/
-package io.element.android.features.call
+package io.element.android.features.call.impl.services
import android.app.Service
import android.content.Context
import android.content.Intent
+import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
-import io.element.android.features.call.ui.ElementCallActivity
+import io.element.android.features.call.impl.R
+import io.element.android.features.call.impl.ui.ElementCallActivity
import io.element.android.libraries.designsystem.utils.CommonDrawables
+import io.element.android.libraries.push.api.notifications.ForegroundServiceType
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
+import timber.log.Timber
+/**
+ * A foreground service that shows a notification for an ongoing call while the UI is in background.
+ */
class CallForegroundService : Service() {
companion object {
fun start(context: Context) {
val intent = Intent(context, CallForegroundService::class.java)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- context.startForegroundService(intent)
- } else {
- context.startService(intent)
- }
+ ContextCompat.startForegroundService(context, intent)
}
fun stop(context: Context) {
@@ -69,7 +75,17 @@ class CallForegroundService : Service() {
.setContentText(getString(R.string.call_foreground_service_message_android))
.setContentIntent(pendingIntent)
.build()
- startForeground(1, notification)
+ val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
+ val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
+ } else {
+ 0
+ }
+ runCatching {
+ ServiceCompat.startForeground(this, notificationId, notification, serviceType)
+ }.onFailure {
+ Timber.e(it, "Failed to start ongoing call foreground service")
+ }
}
override fun onDestroy() {
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
similarity index 80%
rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
index d16baacf3e..a4d60549c9 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package io.element.android.features.call.ui
+package io.element.android.features.call.impl.ui
-import io.element.android.features.call.utils.WidgetMessageInterceptor
+import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
sealed interface CallScreenEvents {
data object Hangup : CallScreenEvents
- data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
+ data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) :
+ CallScreenEvents
}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
similarity index 91%
rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
index 49f6352212..a2e8359284 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.ui
+package io.element.android.features.call.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -30,11 +30,12 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.features.call.CallType
-import io.element.android.features.call.data.WidgetMessage
-import io.element.android.features.call.utils.CallWidgetProvider
-import io.element.android.features.call.utils.WidgetMessageInterceptor
-import io.element.android.features.call.utils.WidgetMessageSerializer
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.data.WidgetMessage
+import io.element.android.features.call.impl.utils.ActiveCallManager
+import io.element.android.features.call.impl.utils.CallWidgetProvider
+import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
+import io.element.android.features.call.impl.utils.WidgetMessageSerializer
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -65,6 +66,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val matrixClientsProvider: MatrixClientProvider,
private val screenTracker: ScreenTracker,
private val appCoroutineScope: CoroutineScope,
+ private val activeCallManager: ActiveCallManager,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -84,6 +86,10 @@ class CallScreenPresenter @AssistedInject constructor(
LaunchedEffect(Unit) {
loadUrl(callType, urlState, callWidgetDriver)
+
+ if (callType is CallType.RoomCall) {
+ activeCallManager.joinedCall(callType.sessionId, callType.roomId)
+ }
}
when (callType) {
@@ -134,6 +140,14 @@ class CallScreenPresenter @AssistedInject constructor(
}
}
+ DisposableEffect(Unit) {
+ onDispose {
+ if (callType is CallType.RoomCall) {
+ activeCallManager.hungUpCall()
+ }
+ }
+ }
+
fun handleEvents(event: CallScreenEvents) {
when (event) {
is CallScreenEvents.Hangup -> {
@@ -193,7 +207,6 @@ class CallScreenPresenter @AssistedInject constructor(
val client = (callType as? CallType.RoomCall)?.sessionId?.let {
matrixClientsProvider.getOrNull(it)
} ?: return@DisposableEffect onDispose { }
-
coroutineScope.launch {
client.syncService().syncState
.onEach { state ->
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
similarity index 94%
rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
index 76926bfb9f..9b0931d5fd 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.ui
+package io.element.android.features.call.impl.ui
import io.element.android.libraries.architecture.AsyncData
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
similarity index 96%
rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
index cc62ce03e1..ebf006d102 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.ui
+package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint
import android.view.ViewGroup
@@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.call.R
-import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor
+import io.element.android.features.call.impl.R
+import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
similarity index 86%
rename from features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
index f810dbb496..8d6324c86a 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package io.element.android.features.call.ui
+package io.element.android.features.call.impl.ui
import android.Manifest
-import android.content.Context
import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.res.Configuration
import android.media.AudioAttributes
import android.media.AudioFocusRequest
@@ -41,30 +39,16 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme
-import io.element.android.features.call.CallForegroundService
-import io.element.android.features.call.CallType
-import io.element.android.features.call.di.CallBindings
-import io.element.android.features.call.utils.CallIntentDataParser
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.DefaultElementCallEntryPoint
+import io.element.android.features.call.impl.di.CallBindings
+import io.element.android.features.call.impl.services.CallForegroundService
+import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.architecture.bindings
import javax.inject.Inject
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
- companion object {
- private const val EXTRA_CALL_WIDGET_SETTINGS = "EXTRA_CALL_WIDGET_SETTINGS"
-
- fun start(
- context: Context,
- callInputs: CallType,
- ) {
- val intent = Intent(context, ElementCallActivity::class.java).apply {
- putExtra(EXTRA_CALL_WIDGET_SETTINGS, callInputs)
- addFlags(FLAG_ACTIVITY_NEW_TASK)
- }
- context.startActivity(intent)
- }
- }
-
@Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var appPreferencesStore: AppPreferencesStore
@@ -88,7 +72,13 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
applicationContext.bindings().inject(this)
- window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ @Suppress("DEPRECATION")
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
setCallType(intent)
@@ -157,16 +147,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
}
private fun setCallType(intent: Intent?) {
- val inputs = intent?.let {
- IntentCompat.getParcelableExtra(it, EXTRA_CALL_WIDGET_SETTINGS, CallType::class.java)
+ val callType = intent?.let {
+ IntentCompat.getParcelableExtra(it, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallType::class.java)
}
val intentUrl = intent?.dataString?.let(::parseUrl)
when {
// Re-opened the activity but we have no url to load or a cached one, finish the activity
- intent?.dataString == null && inputs == null && webViewTarget.value == null -> finish()
- inputs != null -> {
- webViewTarget.value = inputs
- presenter = presenterFactory.create(inputs, this)
+ intent?.dataString == null && callType == null && webViewTarget.value == null -> finish()
+ callType != null -> {
+ webViewTarget.value = callType
+ presenter = presenterFactory.create(callType, this)
}
intentUrl != null -> {
val fallbackInputs = CallType.ExternalUrl(intentUrl)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
new file mode 100644
index 0000000000..cc39dd124f
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.ui
+
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.IntentCompat
+import androidx.lifecycle.lifecycleScope
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
+import io.element.android.features.call.impl.di.CallBindings
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.features.call.impl.utils.ActiveCallManager
+import io.element.android.features.call.impl.utils.CallState
+import io.element.android.libraries.architecture.bindings
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import javax.inject.Inject
+
+/**
+ * Activity that's displayed as a full screen intent when an incoming call is received.
+ */
+class IncomingCallActivity : AppCompatActivity() {
+ companion object {
+ /**
+ * Extra key for the notification data.
+ */
+ const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA"
+ }
+
+ @Inject
+ lateinit var elementCallEntryPoint: ElementCallEntryPoint
+
+ @Inject
+ lateinit var activeCallManager: ActiveCallManager
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ applicationContext.bindings().inject(this)
+
+ // Set flags so it can be displayed in the lock screen
+ @Suppress("DEPRECATION")
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
+
+ val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
+ if (notificationData != null) {
+ setContent {
+ IncomingCallScreen(
+ notificationData = notificationData,
+ onAnswer = ::onAnswer,
+ onCancel = ::onCancel,
+ )
+ }
+ } else {
+ // No data, finish the activity
+ finish()
+ return
+ }
+
+ activeCallManager.activeCall
+ .filter { it?.callState !is CallState.Ringing }
+ .onEach { finish() }
+ .launchIn(lifecycleScope)
+ }
+
+ private fun onAnswer(notificationData: CallNotificationData) {
+ elementCallEntryPoint.startCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
+ }
+
+ private fun onCancel() {
+ activeCallManager.hungUpCall()
+ }
+}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
new file mode 100644
index 0000000000..80dc2353ca
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.ui
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.call.impl.R
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.libraries.designsystem.background.OnboardingBackground
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.core.EventId
+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.UserId
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@Composable
+internal fun IncomingCallScreen(
+ notificationData: CallNotificationData,
+ onAnswer: (CallNotificationData) -> Unit,
+ onCancel: () -> Unit,
+) {
+ ElementTheme {
+ OnboardingBackground()
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 124.dp)
+ .weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Avatar(
+ avatarData = AvatarData(
+ id = notificationData.senderId.value,
+ name = notificationData.senderName,
+ url = notificationData.avatarUrl,
+ size = AvatarSize.IncomingCall,
+ )
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(
+ text = notificationData.senderName ?: notificationData.senderId.value,
+ style = ElementTheme.typography.fontHeadingMdBold,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(R.string.screen_incoming_call_subtitle_android),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textSecondary,
+ textAlign = TextAlign.Center,
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ActionButton(
+ size = 64.dp,
+ onClick = { onAnswer(notificationData) },
+ icon = CompoundIcons.VoiceCall(),
+ title = stringResource(CommonStrings.action_accept),
+ backgroundColor = ElementTheme.colors.iconSuccessPrimary,
+ borderColor = ElementTheme.colors.borderSuccessSubtle
+ )
+
+ ActionButton(
+ size = 64.dp,
+ onClick = onCancel,
+ icon = CompoundIcons.EndCall(),
+ title = stringResource(CommonStrings.action_reject),
+ backgroundColor = ElementTheme.colors.iconCriticalPrimary,
+ borderColor = ElementTheme.colors.borderCriticalSubtle
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ActionButton(
+ size: Dp,
+ onClick: () -> Unit,
+ icon: ImageVector,
+ title: String,
+ backgroundColor: Color,
+ borderColor: Color,
+ contentDescription: String? = title,
+ borderSize: Dp = 1.33.dp,
+) {
+ Column(
+ modifier = Modifier.width(120.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ FilledIconButton(
+ modifier = Modifier.size(size + borderSize)
+ .border(borderSize, borderColor, CircleShape),
+ onClick = onClick,
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = backgroundColor,
+ contentColor = Color.White,
+ )
+ ) {
+ Icon(
+ modifier = Modifier.size(32.dp),
+ imageVector = icon,
+ contentDescription = contentDescription
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = title,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ color = ElementTheme.colors.textPrimary,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun IncomingCallScreenPreview() {
+ ElementPreview {
+ IncomingCallScreen(
+ notificationData = CallNotificationData(
+ sessionId = SessionId("@alice:matrix.org"),
+ roomId = RoomId("!1234:matrix.org"),
+ eventId = EventId("\$asdadadsad:matrix.org"),
+ senderId = UserId("@bob:matrix.org"),
+ roomName = "A room",
+ senderName = "Bob",
+ avatarUrl = null,
+ notificationChannelId = "incoming_call",
+ timestamp = 0L,
+ ),
+ onAnswer = {},
+ onCancel = {},
+ )
+ }
+}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
new file mode 100644
index 0000000000..7f2f601bd6
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.utils
+
+import android.annotation.SuppressLint
+import androidx.core.app.NotificationManagerCompat
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.appconfig.ElementCallConfig
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.matrix.api.MatrixClientProvider
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.push.api.notifications.ForegroundServiceType
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
+import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Manages the active call state.
+ */
+interface ActiveCallManager {
+ /**
+ * The active call state flow, which will be updated when the active call changes.
+ */
+ val activeCall: StateFlow
+
+ /**
+ * Registers an incoming call if there isn't an existing active call and posts a [CallState.Ringing] notification.
+ * @param notificationData The data for the incoming call notification.
+ */
+ fun registerIncomingCall(notificationData: CallNotificationData)
+
+ /**
+ * Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification.
+ */
+ fun incomingCallTimedOut()
+
+ /**
+ * Hangs up the active call and removes any associated UI.
+ */
+ fun hungUpCall()
+
+ /**
+ * Called when the user joins a call. It will remove any existing UI and set the call state as [CallState.InCall].
+ *
+ * @param sessionId The session ID of the user joining the call.
+ * @param roomId The room ID of the call.
+ */
+ fun joinedCall(sessionId: SessionId, roomId: RoomId)
+}
+
+@SingleIn(AppScope::class)
+@ContributesBinding(AppScope::class)
+class DefaultActiveCallManager @Inject constructor(
+ private val coroutineScope: CoroutineScope,
+ private val matrixClientProvider: MatrixClientProvider,
+ private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
+ private val ringingCallNotificationCreator: RingingCallNotificationCreator,
+ private val notificationManagerCompat: NotificationManagerCompat,
+) : ActiveCallManager {
+ private var timedOutCallJob: Job? = null
+
+ override val activeCall = MutableStateFlow(null)
+
+ override fun registerIncomingCall(notificationData: CallNotificationData) {
+ if (activeCall.value != null) {
+ Timber.w("Already have an active call, ignoring incoming call: $notificationData")
+ return
+ }
+ activeCall.value = ActiveCall(
+ sessionId = notificationData.sessionId,
+ roomId = notificationData.roomId,
+ callState = CallState.Ringing(notificationData),
+ )
+
+ timedOutCallJob = coroutineScope.launch {
+ registerIncomingCall(notificationData)
+ showIncomingCallNotification(notificationData)
+
+ // Wait for the call to end
+ delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
+ incomingCallTimedOut()
+ }
+ }
+
+ override fun incomingCallTimedOut() {
+ val previousActiveCall = activeCall.value ?: return
+ val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
+ activeCall.value = null
+
+ cancelIncomingCallNotification()
+
+ coroutineScope.launch {
+ onMissedCallNotificationHandler.addMissedCallNotification(
+ sessionId = previousActiveCall.sessionId,
+ roomId = previousActiveCall.roomId,
+ eventId = notificationData.eventId,
+ )
+ }
+ }
+
+ override fun hungUpCall() {
+ cancelIncomingCallNotification()
+ timedOutCallJob?.cancel()
+ activeCall.value = null
+ }
+
+ override fun joinedCall(sessionId: SessionId, roomId: RoomId) {
+ cancelIncomingCallNotification()
+ timedOutCallJob?.cancel()
+
+ activeCall.value = ActiveCall(
+ sessionId = sessionId,
+ roomId = roomId,
+ callState = CallState.InCall,
+ )
+ // Send call notification to the room
+ coroutineScope.launch {
+ matrixClientProvider.getOrRestore(sessionId)
+ .getOrNull()
+ ?.getRoom(roomId)
+ ?.sendCallNotificationIfNeeded()
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) {
+ val notification = ringingCallNotificationCreator.createNotification(
+ sessionId = notificationData.sessionId,
+ roomId = notificationData.roomId,
+ eventId = notificationData.eventId,
+ senderId = notificationData.senderId,
+ roomName = notificationData.roomName,
+ senderDisplayName = notificationData.senderName ?: notificationData.senderId.value,
+ roomAvatarUrl = notificationData.avatarUrl,
+ notificationChannelId = notificationData.notificationChannelId,
+ timestamp = notificationData.timestamp
+ ) ?: return
+ runCatching {
+ notificationManagerCompat.notify(
+ NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL),
+ notification,
+ )
+ }.onFailure {
+ Timber.e(it, "Failed to publish notification for incoming call")
+ }
+ }
+
+ private fun cancelIncomingCallNotification() {
+ notificationManagerCompat.cancel(NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL))
+ }
+}
+
+/**
+ * Represents an active call.
+ */
+data class ActiveCall(
+ val sessionId: SessionId,
+ val roomId: RoomId,
+ val callState: CallState,
+)
+
+/**
+ * Represents the state of an active call.
+ */
+sealed interface CallState {
+ /**
+ * The call is in a ringing state.
+ * @param notificationData The data for the incoming call notification.
+ */
+ data class Ringing(val notificationData: CallNotificationData) : CallState
+
+ /**
+ * The call is in an in-call state.
+ */
+ data object InCall : CallState
+}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
similarity index 98%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
index c9f4532951..fcbd535a37 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
import android.net.Uri
import javax.inject.Inject
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
similarity index 95%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
index b65298854d..670571476c 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
similarity index 97%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
index 7fb6d3cb48..1daa0a8f3d 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt
new file mode 100644
index 0000000000..bc5816220a
--- /dev/null
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2024 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.features.call.impl.utils
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.PendingIntentCompat
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.DefaultElementCallEntryPoint
+import io.element.android.features.call.impl.ui.ElementCallActivity
+
+internal object IntentProvider {
+ fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply {
+ putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+ }
+
+ fun getPendingIntent(context: Context, callType: CallType): PendingIntent {
+ return PendingIntentCompat.getActivity(
+ context,
+ DefaultElementCallEntryPoint.REQUEST_CODE,
+ createIntent(context, callType),
+ 0,
+ false
+ )!!
+ }
+}
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
similarity index 97%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
index c4676ac9dc..5332c75f6c 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
import android.graphics.Bitmap
import android.webkit.JavascriptInterface
@@ -22,7 +22,7 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
-import io.element.android.features.call.BuildConfig
+import io.element.android.features.call.impl.BuildConfig
import kotlinx.coroutines.flow.MutableSharedFlow
class WebViewWidgetMessageInterceptor(
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
similarity index 93%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
index fa5c3bea67..6a6ca5788a 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageInterceptor.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
import kotlinx.coroutines.flow.Flow
diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
similarity index 89%
rename from features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
index aa7424e9a3..3d97a31ceb 100644
--- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WidgetMessageSerializer.kt
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package io.element.android.features.call.utils
+package io.element.android.features.call.impl.utils
-import io.element.android.features.call.data.WidgetMessage
+import io.element.android.features.call.impl.data.WidgetMessage
import kotlinx.serialization.json.Json
object WidgetMessageSerializer {
diff --git a/features/call/src/main/res/values-be/translations.xml b/features/call/impl/src/main/res/values-be/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-be/translations.xml
rename to features/call/impl/src/main/res/values-be/translations.xml
diff --git a/features/call/src/main/res/values-cs/translations.xml b/features/call/impl/src/main/res/values-cs/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-cs/translations.xml
rename to features/call/impl/src/main/res/values-cs/translations.xml
diff --git a/features/call/src/main/res/values-de/translations.xml b/features/call/impl/src/main/res/values-de/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-de/translations.xml
rename to features/call/impl/src/main/res/values-de/translations.xml
diff --git a/features/call/src/main/res/values-es/translations.xml b/features/call/impl/src/main/res/values-es/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-es/translations.xml
rename to features/call/impl/src/main/res/values-es/translations.xml
diff --git a/features/call/src/main/res/values-fr/translations.xml b/features/call/impl/src/main/res/values-fr/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-fr/translations.xml
rename to features/call/impl/src/main/res/values-fr/translations.xml
diff --git a/features/call/src/main/res/values-hu/translations.xml b/features/call/impl/src/main/res/values-hu/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-hu/translations.xml
rename to features/call/impl/src/main/res/values-hu/translations.xml
diff --git a/features/call/src/main/res/values-in/translations.xml b/features/call/impl/src/main/res/values-in/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-in/translations.xml
rename to features/call/impl/src/main/res/values-in/translations.xml
diff --git a/features/call/src/main/res/values-it/translations.xml b/features/call/impl/src/main/res/values-it/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-it/translations.xml
rename to features/call/impl/src/main/res/values-it/translations.xml
diff --git a/features/call/src/main/res/values-ka/translations.xml b/features/call/impl/src/main/res/values-ka/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-ka/translations.xml
rename to features/call/impl/src/main/res/values-ka/translations.xml
diff --git a/features/call/src/main/res/values-pt/translations.xml b/features/call/impl/src/main/res/values-pt/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-pt/translations.xml
rename to features/call/impl/src/main/res/values-pt/translations.xml
diff --git a/features/call/src/main/res/values-ro/translations.xml b/features/call/impl/src/main/res/values-ro/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-ro/translations.xml
rename to features/call/impl/src/main/res/values-ro/translations.xml
diff --git a/features/call/src/main/res/values-ru/translations.xml b/features/call/impl/src/main/res/values-ru/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-ru/translations.xml
rename to features/call/impl/src/main/res/values-ru/translations.xml
diff --git a/features/call/src/main/res/values-sk/translations.xml b/features/call/impl/src/main/res/values-sk/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-sk/translations.xml
rename to features/call/impl/src/main/res/values-sk/translations.xml
diff --git a/features/call/src/main/res/values-sv/translations.xml b/features/call/impl/src/main/res/values-sv/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-sv/translations.xml
rename to features/call/impl/src/main/res/values-sv/translations.xml
diff --git a/features/call/src/main/res/values-uk/translations.xml b/features/call/impl/src/main/res/values-uk/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-uk/translations.xml
rename to features/call/impl/src/main/res/values-uk/translations.xml
diff --git a/features/call/src/main/res/values-zh-rTW/translations.xml b/features/call/impl/src/main/res/values-zh-rTW/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-zh-rTW/translations.xml
rename to features/call/impl/src/main/res/values-zh-rTW/translations.xml
diff --git a/features/call/src/main/res/values-zh/translations.xml b/features/call/impl/src/main/res/values-zh/translations.xml
similarity index 100%
rename from features/call/src/main/res/values-zh/translations.xml
rename to features/call/impl/src/main/res/values-zh/translations.xml
diff --git a/features/call/src/main/res/values/do_not_translate.xml b/features/call/impl/src/main/res/values/do_not_translate.xml
similarity index 100%
rename from features/call/src/main/res/values/do_not_translate.xml
rename to features/call/impl/src/main/res/values/do_not_translate.xml
diff --git a/features/call/src/main/res/values/localazy.xml b/features/call/impl/src/main/res/values/localazy.xml
similarity index 81%
rename from features/call/src/main/res/values/localazy.xml
rename to features/call/impl/src/main/res/values/localazy.xml
index cfe40526f4..5a386b2416 100644
--- a/features/call/src/main/res/values/localazy.xml
+++ b/features/call/impl/src/main/res/values/localazy.xml
@@ -3,4 +3,5 @@
"Ongoing call"
"Tap to return to the call"
"☎️ Call in progress"
+ "Incoming Element Call"
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt
new file mode 100644
index 0000000000..c78b0bbe20
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024 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.features.call
+
+import android.content.Intent
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.DefaultElementCallEntryPoint
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.features.call.impl.ui.ElementCallActivity
+import io.element.android.features.call.utils.FakeActiveCallManager
+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.matrix.test.A_USER_ID_2
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(RobolectricTestRunner::class)
+class DefaultElementCallEntryPointTest {
+ @Test
+ fun `startCall - starts ElementCallActivity setup with the needed extras`() {
+ val entryPoint = createEntryPoint()
+ entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
+
+ val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java)
+ val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
+ assertThat(intent.component).isEqualTo(expectedIntent.component)
+ assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue()
+ }
+
+ @Test
+ fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() {
+ val registerIncomingCallLambda = lambdaRecorder {}
+ val activeCallManager = FakeActiveCallManager(registerIncomingCallResult = registerIncomingCallLambda)
+ val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
+
+ entryPoint.handleIncomingCall(
+ callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
+ eventId = AN_EVENT_ID,
+ senderId = A_USER_ID_2,
+ roomName = "roomName",
+ senderName = "senderName",
+ avatarUrl = "avatarUrl",
+ timestamp = 0,
+ notificationChannelId = "notificationChannelId",
+ )
+
+ registerIncomingCallLambda.assertions().isCalledOnce()
+ }
+
+ private fun createEntryPoint(
+ activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
+ ) = DefaultElementCallEntryPoint(
+ context = InstrumentationRegistry.getInstrumentation().targetContext,
+ activeCallManager = activeCallManager,
+ )
+}
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
similarity index 95%
rename from features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
index 6a24698efa..359483520a 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt
@@ -19,7 +19,7 @@ package io.element.android.features.call
import android.Manifest
import android.webkit.PermissionRequest
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.call.ui.mapWebkitPermissions
+import io.element.android.features.call.impl.ui.mapWebkitPermissions
import org.junit.Test
class MapWebkitPermissionsTest {
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt
new file mode 100644
index 0000000000..40cdc0522f
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2024 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.features.call.notifications
+
+import androidx.core.graphics.drawable.IconCompat
+import androidx.test.platform.app.InstrumentationRegistry
+import coil.ImageLoader
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
+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.matrix.test.A_USER_ID_2
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
+import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
+import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class RingingCallNotificationCreatorTest {
+ @Test
+ fun `createNotification - with no associated MatrixClient does nothing`() = runTest {
+ val notificationCreator = createRingingCallNotificationCreator(
+ matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("No client found")) })
+ )
+
+ val result = notificationCreator.createTestNotification()
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun `createNotification - creates a valid notification`() = runTest {
+ val notificationCreator = createRingingCallNotificationCreator(
+ matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) })
+ )
+
+ val result = notificationCreator.createTestNotification()
+
+ assertThat(result).isNotNull()
+ }
+
+ @Test
+ fun `createNotification - tries to load the avatar URL`() = runTest {
+ val getUserIconLambda = lambdaRecorder { _, _ -> null }
+ val notificationCreator = createRingingCallNotificationCreator(
+ matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(FakeMatrixClient()) }),
+ notificationBitmapLoader = FakeNotificationBitmapLoader(getUserIconResult = getUserIconLambda)
+ )
+
+ notificationCreator.createTestNotification()
+
+ getUserIconLambda.assertions().isCalledOnce()
+ }
+
+ private suspend fun RingingCallNotificationCreator.createTestNotification() = createNotification(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ eventId = AN_EVENT_ID,
+ senderId = A_USER_ID_2,
+ roomName = "Room",
+ senderDisplayName = "Johnnie Murphy",
+ roomAvatarUrl = "https://example.com/avatar.jpg",
+ notificationChannelId = "channelId",
+ timestamp = 0L,
+ )
+
+ private fun createRingingCallNotificationCreator(
+ matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
+ imageLoaderHolder: FakeImageLoaderHolder = FakeImageLoaderHolder(),
+ notificationBitmapLoader: FakeNotificationBitmapLoader = FakeNotificationBitmapLoader(),
+ ) = RingingCallNotificationCreator(
+ context = InstrumentationRegistry.getInstrumentation().targetContext,
+ matrixClientProvider = matrixClientProvider,
+ imageLoaderHolder = imageLoaderHolder,
+ notificationBitmapLoader = notificationBitmapLoader,
+ )
+}
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
similarity index 96%
rename from features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
index 30579ad99c..b734a8ff5c 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
@@ -21,7 +21,11 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.MobileScreen
-import io.element.android.features.call.CallType
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.impl.ui.CallScreenEvents
+import io.element.android.features.call.impl.ui.CallScreenNavigator
+import io.element.android.features.call.impl.ui.CallScreenPresenter
+import io.element.android.features.call.utils.FakeActiveCallManager
import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData
@@ -254,6 +258,7 @@ class CallScreenPresenterTest {
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
+ activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
@@ -270,8 +275,9 @@ class CallScreenPresenterTest {
clock = clock,
dispatchers = dispatchers,
matrixClientsProvider = matrixClientsProvider,
- screenTracker = screenTracker,
appCoroutineScope = this,
+ activeCallManager = activeCallManager,
+ screenTracker = screenTracker,
)
}
}
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
similarity index 92%
rename from features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
index 23e0cf9027..c280a1a61d 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt
@@ -16,6 +16,8 @@
package io.element.android.features.call.ui
+import io.element.android.features.call.impl.ui.CallScreenNavigator
+
class FakeCallScreenNavigator : CallScreenNavigator {
var closeCalled = false
private set
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
similarity index 99%
rename from features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
index 093b6aab3b..595f5aa41d 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt
@@ -17,6 +17,7 @@
package io.element.android.features.call.utils
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.impl.utils.CallIntentDataParser
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
new file mode 100644
index 0000000000..3ae95665be
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2024 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.features.call.utils
+
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
+import io.element.android.features.call.impl.utils.ActiveCall
+import io.element.android.features.call.impl.utils.CallState
+import io.element.android.features.call.impl.utils.DefaultActiveCallManager
+import io.element.android.features.call.test.aCallNotificationData
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_ROOM_ID_2
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.push.api.notifications.ForegroundServiceType
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
+import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
+import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
+import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DefaultActiveCallManagerTest {
+ private val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL)
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `registerIncomingCall - sets the incoming call as active`() = runTest {
+ val notificationManagerCompat = mockk(relaxed = true)
+ val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
+
+ assertThat(manager.activeCall.value).isNull()
+
+ val callNotificationData = aCallNotificationData()
+ manager.registerIncomingCall(callNotificationData)
+
+ assertThat(manager.activeCall.value).isEqualTo(
+ ActiveCall(
+ sessionId = callNotificationData.sessionId,
+ roomId = callNotificationData.roomId,
+ callState = CallState.Ringing(callNotificationData)
+ )
+ )
+
+ runCurrent()
+
+ verify { notificationManagerCompat.notify(notificationId, any()) }
+ }
+
+ @Test
+ fun `registerIncomingCall - when there is an already active call does nothing`() = runTest {
+ val manager = createActiveCallManager()
+
+ // Register existing call
+ val callNotificationData = aCallNotificationData()
+ manager.registerIncomingCall(callNotificationData)
+ val activeCall = manager.activeCall.value
+
+ // Now add a new call
+ manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
+
+ assertThat(manager.activeCall.value).isEqualTo(activeCall)
+ assertThat(manager.activeCall.value?.roomId).isNotEqualTo(A_ROOM_ID_2)
+ }
+
+ @Test
+ fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
+ val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> }
+ val manager = createActiveCallManager(
+ onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
+ )
+
+ manager.incomingCallTimedOut()
+
+ addMissedCallNotificationLambda.assertions().isNeverCalled()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest {
+ val notificationManagerCompat = mockk(relaxed = true)
+ val addMissedCallNotificationLambda = lambdaRecorder { _, _, _ -> }
+ val manager = createActiveCallManager(
+ onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
+ notificationManagerCompat = notificationManagerCompat,
+ )
+
+ manager.registerIncomingCall(aCallNotificationData())
+ assertThat(manager.activeCall.value).isNotNull()
+
+ manager.incomingCallTimedOut()
+ assertThat(manager.activeCall.value).isNull()
+
+ runCurrent()
+
+ addMissedCallNotificationLambda.assertions().isCalledOnce()
+
+ verify { notificationManagerCompat.cancel(notificationId) }
+ }
+
+ @Test
+ fun `hungUpCall - removes existing call`() = runTest {
+ val notificationManagerCompat = mockk(relaxed = true)
+ val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
+
+ manager.registerIncomingCall(aCallNotificationData())
+ assertThat(manager.activeCall.value).isNotNull()
+
+ manager.hungUpCall()
+ assertThat(manager.activeCall.value).isNull()
+
+ verify { notificationManagerCompat.cancel(notificationId) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
+ val notificationManagerCompat = mockk(relaxed = true)
+ val sendCallNotifyLambda = lambdaRecorder> { Result.success(Unit) }
+ val room = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotifyLambda)
+ val client = FakeMatrixClient().apply {
+ givenGetRoomResult(A_ROOM_ID, room)
+ }
+ val manager = createActiveCallManager(
+ matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
+ notificationManagerCompat = notificationManagerCompat,
+ )
+ assertThat(manager.activeCall.value).isNull()
+
+ manager.joinedCall(A_SESSION_ID, A_ROOM_ID)
+ assertThat(manager.activeCall.value).isEqualTo(
+ ActiveCall(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ callState = CallState.InCall,
+ )
+ )
+
+ runCurrent()
+
+ sendCallNotifyLambda.assertions().isCalledOnce()
+ verify { notificationManagerCompat.cancel(notificationId) }
+ }
+
+ private fun TestScope.createActiveCallManager(
+ matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
+ onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
+ notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
+ ) = DefaultActiveCallManager(
+ coroutineScope = this,
+ matrixClientProvider = matrixClientProvider,
+ onMissedCallNotificationHandler = onMissedCallNotificationHandler,
+ ringingCallNotificationCreator = RingingCallNotificationCreator(
+ context = InstrumentationRegistry.getInstrumentation().targetContext,
+ matrixClientProvider = matrixClientProvider,
+ imageLoaderHolder = FakeImageLoaderHolder(),
+ notificationBitmapLoader = FakeNotificationBitmapLoader(),
+ ),
+ notificationManagerCompat = notificationManagerCompat,
+ )
+}
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
similarity index 98%
rename from features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
index 0b7e2ce953..a41b5f0296 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt
@@ -17,6 +17,7 @@
package io.element.android.features.call.utils
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt
new file mode 100644
index 0000000000..763d7bf8d5
--- /dev/null
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 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.features.call.utils
+
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.features.call.impl.utils.ActiveCall
+import io.element.android.features.call.impl.utils.ActiveCallManager
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeActiveCallManager(
+ var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
+ var incomingCallTimedOutResult: () -> Unit = {},
+ var hungUpCallResult: () -> Unit = {},
+ var joinedCallResult: (SessionId, RoomId) -> Unit = { _, _ -> },
+) : ActiveCallManager {
+ override val activeCall = MutableStateFlow(null)
+
+ override fun registerIncomingCall(notificationData: CallNotificationData) {
+ registerIncomingCallResult(notificationData)
+ }
+
+ override fun incomingCallTimedOut() {
+ incomingCallTimedOutResult()
+ }
+
+ override fun hungUpCall() {
+ hungUpCallResult()
+ }
+
+ override fun joinedCall(sessionId: SessionId, roomId: RoomId) {
+ joinedCallResult(sessionId, roomId)
+ }
+
+ fun setActiveCall(value: ActiveCall?) {
+ this.activeCall.value = value
+ }
+}
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
similarity index 95%
rename from features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
index c9e9ebb2ae..b957122a3f 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt
@@ -16,6 +16,7 @@
package io.element.android.features.call.utils
+import io.element.android.features.call.impl.utils.CallWidgetProvider
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.widget.MatrixWidgetDriver
diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
similarity index 93%
rename from features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
rename to features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
index 5a312250b0..0ef5046028 100644
--- a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt
@@ -16,6 +16,7 @@
package io.element.android.features.call.utils
+import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
import kotlinx.coroutines.flow.MutableSharedFlow
class FakeWidgetMessageInterceptor : WidgetMessageInterceptor {
diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts
new file mode 100644
index 0000000000..729811f434
--- /dev/null
+++ b/features/call/test/build.gradle.kts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 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.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.call.test"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.core)
+
+ api(projects.features.call.api)
+ implementation(projects.features.call.impl)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrix.test)
+}
diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt
new file mode 100644
index 0000000000..c69e4b9e6c
--- /dev/null
+++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2024 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.features.call.test
+
+import io.element.android.features.call.impl.notifications.CallNotificationData
+import io.element.android.libraries.matrix.api.core.EventId
+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.UserId
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_ROOM_NAME
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.A_USER_ID_2
+import io.element.android.libraries.matrix.test.A_USER_NAME
+
+fun aCallNotificationData(
+ sessionId: SessionId = A_SESSION_ID,
+ roomId: RoomId = A_ROOM_ID,
+ eventId: EventId = AN_EVENT_ID,
+ senderId: UserId = A_USER_ID_2,
+ roomName: String = A_ROOM_NAME,
+ senderName: String? = A_USER_NAME,
+ avatarUrl: String? = AN_AVATAR_URL,
+ notificationChannelId: String = "channel_id",
+ timestamp: Long = 0L,
+): CallNotificationData = CallNotificationData(
+ sessionId = sessionId,
+ roomId = roomId,
+ eventId = eventId,
+ senderId = senderId,
+ roomName = roomName,
+ senderName = senderName,
+ avatarUrl = avatarUrl,
+ notificationChannelId = notificationChannelId,
+ timestamp = timestamp,
+)
diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt
new file mode 100644
index 0000000000..cda9cf98f0
--- /dev/null
+++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024 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.features.call.test
+
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+
+class FakeElementCallEntryPoint(
+ var startCallResult: (CallType) -> Unit = {},
+ var handleIncomingCallResult: (CallType.RoomCall, EventId, UserId, String?, String?, String?, String) -> Unit = { _, _, _, _, _, _, _ -> }
+) : ElementCallEntryPoint {
+ override fun startCall(callType: CallType) {
+ startCallResult(callType)
+ }
+
+ override fun handleIncomingCall(
+ callType: CallType.RoomCall,
+ eventId: EventId,
+ senderId: UserId,
+ roomName: String?,
+ senderName: String?,
+ avatarUrl: String?,
+ timestamp: Long,
+ notificationChannelId: String
+ ) {
+ handleIncomingCallResult(callType, eventId, senderId, roomName, senderName, avatarUrl, notificationChannelId)
+ }
+}
diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts
index df0c4dff7a..6032d195e6 100644
--- a/features/messages/impl/build.gradle.kts
+++ b/features/messages/impl/build.gradle.kts
@@ -39,7 +39,7 @@ dependencies {
anvil(projects.anvilcodegen)
api(projects.features.messages.api)
implementation(projects.appconfig)
- implementation(projects.features.call)
+ implementation(projects.features.call.api)
implementation(projects.features.location.api)
implementation(projects.features.poll.api)
implementation(projects.libraries.androidutils)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
index 6665c5fa2a..8859c4ae03 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
@@ -16,7 +16,6 @@
package io.element.android.features.messages.impl
-import android.content.Context
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -31,8 +30,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.call.CallType
-import io.element.android.features.call.ui.ElementCallActivity
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint
@@ -58,7 +57,6 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.show
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
@@ -78,11 +76,11 @@ import kotlinx.parcelize.Parcelize
class MessagesFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- @ApplicationContext private val context: Context,
private val matrixClient: MatrixClient,
private val sendLocationEntryPoint: SendLocationEntryPoint,
private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint,
+ private val elementCallEntryPoint: ElementCallEntryPoint,
private val analyticsService: AnalyticsService,
) : BaseFlowNode(
backstack = BackStack(
@@ -188,12 +186,12 @@ class MessagesFlowNode @AssistedInject constructor(
}
override fun onJoinCallClick(roomId: RoomId) {
- val inputs = CallType.RoomCall(
+ val callType = CallType.RoomCall(
sessionId = matrixClient.sessionId,
roomId = roomId,
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
- ElementCallActivity.start(context, inputs)
+ elementCallEntryPoint.startCall(callType)
}
}
val inputs = MessagesNode.Inputs(
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt
index 4b6d28bd89..58cf11c4a7 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt
@@ -173,6 +173,7 @@ class TimelineControllerTest {
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
+ sut.focusOnEvent(AN_EVENT_ID)
awaitItem().also { state ->
assertThat(state).isEqualTo(liveTimeline)
}
diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts
index 037165e4ee..b90a0e679d 100644
--- a/features/roomdetails/impl/build.gradle.kts
+++ b/features/roomdetails/impl/build.gradle.kts
@@ -56,7 +56,7 @@ dependencies {
api(projects.libraries.usersearch.api)
api(projects.services.apperror.api)
implementation(libs.coil.compose)
- implementation(projects.features.call)
+ implementation(projects.features.call.api)
implementation(projects.features.createroom.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
index 9b3cb0f8bc..4a34d64ad0 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
@@ -16,7 +16,6 @@
package io.element.android.features.roomdetails.impl
-import android.content.Context
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -30,8 +29,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.call.CallType
-import io.element.android.features.call.ui.ElementCallActivity
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
@@ -46,7 +45,6 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
@@ -62,8 +60,8 @@ import kotlinx.parcelize.Parcelize
class RoomDetailsFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- @ApplicationContext private val context: Context,
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
+ private val elementCallEntryPoint: ElementCallEntryPoint,
private val room: MatrixRoom,
private val analyticsService: AnalyticsService,
) : BaseFlowNode(
@@ -147,7 +145,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
roomId = room.roomId,
)
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
- ElementCallActivity.start(context, inputs)
+ elementCallEntryPoint.startCall(inputs)
}
}
createNode(buildContext, listOf(roomDetailsCallback))
@@ -195,7 +193,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
override fun onStartCall(dmRoomId: RoomId) {
- ElementCallActivity.start(context, CallType.RoomCall(sessionId = room.sessionId, roomId = dmRoomId))
+ elementCallEntryPoint.startCall(CallType.RoomCall(roomId = dmRoomId, sessionId = room.sessionId))
}
}
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
index 202766e4ac..ab19c0b522 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt
@@ -70,7 +70,10 @@ private fun Content(desktopApplicationName: String) {
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
add(
annotatedTextWithBold(
- text = stringResource(R.string.screen_create_new_recovery_key_list_item_3),
+ text = stringResource(
+ id = R.string.screen_create_new_recovery_key_list_item_3,
+ stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
+ ),
boldText = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
)
)
diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts
index e41524abb2..3837e81f9b 100644
--- a/features/userprofile/impl/build.gradle.kts
+++ b/features/userprofile/impl/build.gradle.kts
@@ -46,7 +46,7 @@ dependencies {
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
- implementation(projects.features.call)
+ implementation(projects.features.call.api)
api(projects.features.userprofile.api)
api(projects.features.userprofile.shared)
implementation(libs.coil.compose)
diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
index 9101254126..d4335f5d3a 100644
--- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
+++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
@@ -16,7 +16,6 @@
package io.element.android.features.userprofile.impl
-import android.content.Context
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -29,8 +28,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
-import io.element.android.features.call.CallType
-import io.element.android.features.call.ui.ElementCallActivity
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
@@ -40,7 +39,6 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.mimetype.MimeTypes
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.media.MediaSource
@@ -53,7 +51,7 @@ import kotlinx.parcelize.Parcelize
class UserProfileFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
- @ApplicationContext private val context: Context,
+ private val elementCallEntryPoint: ElementCallEntryPoint,
private val sessionIdHolder: CurrentSessionIdHolder,
) : BaseFlowNode(
backstack = BackStack(
@@ -84,7 +82,7 @@ class UserProfileFlowNode @AssistedInject constructor(
}
override fun onStartCall(dmRoomId: RoomId) {
- ElementCallActivity.start(context, CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId))
+ elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionIdHolder.current, roomId = dmRoomId))
}
}
val params = UserProfileNode.UserProfileInputs(userId = inputs().userId)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
index 2dc6c8875f..54791f8505 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
enum class AvatarSize(val dp: Dp) {
CurrentUserTopBar(32.dp),
+ IncomingCall(140.dp),
RoomHeader(96.dp),
RoomListItem(52.dp),
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index a4619635c6..7ca6080f11 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -332,5 +332,10 @@ interface MatrixRoom : Closeable {
*/
suspend fun getPermalinkFor(eventId: EventId): Result
+ /**
+ * Send an Element Call started notification if needed.
+ */
+ suspend fun sendCallNotificationIfNeeded(): Result
+
override fun close() = destroy()
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt
index 67b57b6a07..8367f70725 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt
@@ -72,6 +72,7 @@ object EventType {
const val CALL_NEGOTIATE = "m.call.negotiate"
const val CALL_REJECT = "m.call.reject"
const val CALL_HANGUP = "m.call.hangup"
+ const val CALL_NOTIFY = "m.call.notify"
// This type is not processed by the client, just sent to the server
const val CALL_REPLACES = "m.call.replaces"
@@ -94,6 +95,7 @@ object EventType {
type == CALL_SELECT_ANSWER ||
type == CALL_NEGOTIATE ||
type == CALL_REJECT ||
- type == CALL_REPLACES
+ type == CALL_REPLACES ||
+ type == CALL_NOTIFY
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 2827ad366c..aa7a90c540 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -596,6 +596,10 @@ class RustMatrixRoom(
innerRoom.matrixToEventPermalink(eventId.value)
}
+ override suspend fun sendCallNotificationIfNeeded(): Result = runCatching {
+ innerRoom.sendCallNotificationIfNeeded()
+ }
+
private fun createTimeline(
timeline: InnerTimeline,
isLive: Boolean,
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index 4b4a8e1af0..f220144485 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -86,6 +86,7 @@ class FakeMatrixRoom(
override val liveTimeline: Timeline = FakeTimeline(),
private var roomPermalinkResult: () -> Result = { Result.success("room link") },
private var eventPermalinkResult: (EventId) -> Result = { Result.success("event link") },
+ var sendCallNotificationIfNeededResult: () -> Result = { Result.success(Unit) },
canRedactOwn: Boolean = false,
canRedactOther: Boolean = false,
) : MatrixRoom {
@@ -528,6 +529,10 @@ class FakeMatrixRoom(
theme: String?,
): Result = generateWidgetWebViewUrlResult
+ override suspend fun sendCallNotificationIfNeeded(): Result {
+ return sendCallNotificationIfNeededResult()
+ }
+
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult
fun givenRoomMembersState(state: MatrixRoomMembersState) {
diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts
index c4d78b432d..d043254933 100644
--- a/libraries/push/api/build.gradle.kts
+++ b/libraries/push/api/build.gradle.kts
@@ -24,6 +24,7 @@ android {
dependencies {
implementation(libs.androidx.corektx)
implementation(libs.coroutines.core)
+ implementation(libs.coil.compose)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.pushproviders.api)
}
diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt
new file mode 100644
index 0000000000..6b56eb9e2a
--- /dev/null
+++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 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.api.notifications
+
+import android.graphics.Bitmap
+import androidx.core.graphics.drawable.IconCompat
+import coil.ImageLoader
+
+interface NotificationBitmapLoader {
+ /**
+ * Get icon of a room.
+ * @param path mxc url
+ * @param imageLoader Coil image loader
+ */
+ suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap?
+
+ /**
+ * Get icon of a user.
+ * Before Android P, this does nothing because the icon won't be used
+ * @param path mxc url
+ * @param imageLoader Coil image loader
+ */
+ suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat?
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt
similarity index 63%
rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
rename to libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt
index 050edfcc11..0edfe405ac 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
+++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 New Vector Ltd
+ * Copyright (c) 2024 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.
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package io.element.android.libraries.push.impl.notifications
+package io.element.android.libraries.push.api.notifications
import io.element.android.libraries.matrix.api.core.SessionId
-import javax.inject.Inject
import kotlin.math.abs
-class NotificationIdProvider @Inject constructor() {
+object NotificationIdProvider {
fun getSummaryNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
}
@@ -41,16 +40,30 @@ class NotificationIdProvider @Inject constructor() {
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
}
+ fun getCallNotificationId(sessionId: SessionId): Int {
+ return getOffset(sessionId) + ROOM_CALL_NOTIFICATION_ID
+ }
+
+ fun getForegroundServiceNotificationId(type: ForegroundServiceType): Int {
+ return type.id * 10 + FOREGROUND_SERVICE_NOTIFICATION_ID
+ }
+
private fun getOffset(sessionId: SessionId): Int {
// Compute a int from a string with a low risk of collision.
return abs(sessionId.value.hashCode() % 100_000) * 10
}
- companion object {
- private const val FALLBACK_NOTIFICATION_ID = -1
- private const val SUMMARY_NOTIFICATION_ID = 0
- private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
- private const val ROOM_EVENT_NOTIFICATION_ID = 2
- private const val ROOM_INVITATION_NOTIFICATION_ID = 3
- }
+ private const val FALLBACK_NOTIFICATION_ID = -1
+ private const val SUMMARY_NOTIFICATION_ID = 0
+ private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
+ private const val ROOM_EVENT_NOTIFICATION_ID = 2
+ private const val ROOM_INVITATION_NOTIFICATION_ID = 3
+ private const val ROOM_CALL_NOTIFICATION_ID = 3
+
+ private const val FOREGROUND_SERVICE_NOTIFICATION_ID = 4
+}
+
+enum class ForegroundServiceType(val id: Int) {
+ INCOMING_CALL(1),
+ ONGOING_CALL(2),
}
diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt
new file mode 100644
index 0000000000..40bf786536
--- /dev/null
+++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 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.api.notifications
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+
+/**
+ * Handles missed calls by creating a new notification.
+ */
+interface OnMissedCallNotificationHandler {
+ /**
+ * Adds a missed call notification.
+ */
+ suspend fun addMissedCallNotification(
+ sessionId: SessionId,
+ roomId: RoomId,
+ eventId: EventId,
+ )
+}
diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts
index 85b1d80942..8970f1e3d0 100644
--- a/libraries/push/impl/build.gradle.kts
+++ b/libraries/push/impl/build.gradle.kts
@@ -55,6 +55,7 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.troubleshoot.api)
+ implementation(projects.features.call.api)
api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.api)
api(projects.libraries.push.api)
@@ -76,6 +77,7 @@ dependencies {
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.tests.testutils)
+ testImplementation(projects.features.call.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.toolbox.impl)
testImplementation(projects.services.toolbox.test)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt
index 0746e9c5cd..01b9837f3b 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt
@@ -22,6 +22,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import javax.inject.Inject
interface ActiveNotificationsProvider {
@@ -37,7 +38,6 @@ interface ActiveNotificationsProvider {
@ContributesBinding(AppScope::class)
class DefaultActiveNotificationsProvider @Inject constructor(
private val notificationManager: NotificationManagerCompat,
- private val notificationIdProvider: NotificationIdProvider,
) : ActiveNotificationsProvider {
override fun getAllNotifications(): List {
return notificationManager.activeNotifications
@@ -48,22 +48,22 @@ class DefaultActiveNotificationsProvider @Inject constructor(
}
override fun getMembershipNotificationForSession(sessionId: SessionId): List {
- val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
+ val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
}
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List {
- val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId)
+ val notificationId = NotificationIdProvider.getRoomMessagesNotificationId(sessionId)
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
}
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List {
- val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
+ val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
}
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
- val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId)
+ val summaryId = NotificationIdProvider.getSummaryNotificationId(sessionId)
return getNotificationsForSession(sessionId).find { it.id == summaryId }
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
index 78414fddd1..646bbaa621 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
@@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
@@ -51,6 +52,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
+import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
@@ -150,8 +152,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
}
NotificationContent.MessageLike.CallAnswer,
NotificationContent.MessageLike.CallCandidates,
- NotificationContent.MessageLike.CallHangup,
- is NotificationContent.MessageLike.CallNotify -> { // TODO CallNotify will be handled separately in the future
+ NotificationContent.MessageLike.CallHangup -> {
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
null
}
@@ -172,6 +173,44 @@ class DefaultNotifiableEventResolver @Inject constructor(
senderAvatarPath = senderAvatarUrl,
)
}
+ is NotificationContent.MessageLike.CallNotify -> {
+ if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp)) {
+ NotifiableRingingCallEvent(
+ sessionId = userId,
+ roomId = roomId,
+ eventId = eventId,
+ roomName = roomDisplayName,
+ editedEventId = null,
+ canBeReplaced = true,
+ timestamp = this.timestamp,
+ isRedacted = false,
+ isUpdated = false,
+ description = stringProvider.getString(R.string.notification_incoming_call),
+ senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
+ roomAvatarUrl = roomAvatarUrl,
+ callNotifyType = content.type,
+ senderId = content.senderId,
+ senderAvatarUrl = senderAvatarUrl,
+ )
+ } else {
+ // Create a simple message notification event
+ buildNotifiableMessageEvent(
+ sessionId = userId,
+ senderId = content.senderId,
+ roomId = roomId,
+ eventId = eventId,
+ noisy = true,
+ timestamp = this.timestamp,
+ senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
+ body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}",
+ roomName = roomDisplayName,
+ roomIsDirect = isDirect,
+ roomAvatarPath = roomAvatarUrl,
+ senderAvatarPath = senderAvatarUrl,
+ type = EventType.CALL_NOTIFY,
+ )
+ }
+ }
NotificationContent.MessageLike.KeyVerificationAccept,
NotificationContent.MessageLike.KeyVerificationCancel,
NotificationContent.MessageLike.KeyVerificationDone,
@@ -334,7 +373,8 @@ private fun buildNotifiableMessageEvent(
outGoingMessage: Boolean = false,
outGoingMessageFailed: Boolean = false,
isRedacted: Boolean = false,
- isUpdated: Boolean = false
+ isUpdated: Boolean = false,
+ type: String = EventType.MESSAGE,
) = NotifiableMessageEvent(
sessionId = sessionId,
senderId = senderId,
@@ -356,5 +396,6 @@ private fun buildNotifiableMessageEvent(
outGoingMessage = outGoingMessage,
outGoingMessageFailed = outGoingMessageFailed,
isRedacted = isRedacted,
- isUpdated = isUpdated
+ isUpdated = isUpdated,
+ type = type,
)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt
similarity index 86%
rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt
index 97232320c2..94ff9b8139 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationBitmapLoader.kt
@@ -24,23 +24,27 @@ import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import timber.log.Timber
import javax.inject.Inject
-class NotificationBitmapLoader @Inject constructor(
+@ContributesBinding(AppScope::class)
+class DefaultNotificationBitmapLoader @Inject constructor(
@ApplicationContext private val context: Context,
private val sdkIntProvider: BuildVersionSdkIntProvider,
-) {
+) : NotificationBitmapLoader {
/**
* Get icon of a room.
* @param path mxc url
* @param imageLoader Coil image loader
*/
- suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
+ override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
if (path == null) {
return null
}
@@ -67,7 +71,7 @@ class NotificationBitmapLoader @Inject constructor(
* @param path mxc url
* @param imageLoader Coil image loader
*/
- suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
+ override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
return null
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
index 03c1cd21a2..f485e29847 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationStateService
@@ -53,7 +54,6 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.
class DefaultNotificationDrawerManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val notificationRenderer: NotificationRenderer,
- private val notificationIdProvider: NotificationIdProvider,
private val appNavigationStateService: AppNavigationStateService,
coroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
@@ -124,7 +124,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Clear all known message events for a [sessionId].
*/
override fun clearAllMessagesEvents(sessionId: SessionId) {
- notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
+ notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
clearSummaryNotificationIfNeeded(sessionId)
}
@@ -142,7 +142,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Can also be called when a notification for this room is dismissed by the user.
*/
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
- notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
+ notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
clearSummaryNotificationIfNeeded(sessionId)
}
@@ -165,7 +165,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
* Clear the notifications for a single event.
*/
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
- val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
+ val id = NotificationIdProvider.getRoomEventNotificationId(sessionId)
notificationManager.cancel(eventId.value, id)
clearSummaryNotificationIfNeeded(sessionId)
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt
new file mode 100644
index 0000000000..7a62f53d39
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024 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
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultOnMissedCallNotificationHandler @Inject constructor(
+ private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
+ private val notifiableEventResolver: NotifiableEventResolver,
+) : OnMissedCallNotificationHandler {
+ override suspend fun addMissedCallNotification(
+ sessionId: SessionId,
+ roomId: RoomId,
+ eventId: EventId,
+ ) {
+ // Resolve the event and add a notification for it, at this point it should no longer be a ringing one
+ val notifiableEvent = notifiableEventResolver.resolveEvent(sessionId, roomId, eventId)
+ notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
+ }
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt
index aff2cc7fca..a0b7e30218 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt
@@ -49,6 +49,8 @@ interface NotificationDataFactory {
@JvmName("toNotificationSimpleEvents")
@Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(simpleEvents: List): List
+ @JvmName("toNotificationFallbackEvents")
+ @Suppress("INAPPLICABLE_JVM_NAME")
fun toNotifications(fallback: List): List
fun createSummaryNotification(
@@ -130,6 +132,8 @@ class DefaultNotificationDataFactory @Inject constructor(
}
}
+ @JvmName("toNotificationFallbackEvents")
+ @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(fallback: List): List {
return fallback.map { event ->
OneShotNotification(
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
index a1509aaa96..e36966cc9a 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
@@ -19,10 +19,12 @@ package io.element.android.libraries.push.impl.notifications
import coil.ImageLoader
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
+import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import timber.log.Timber
import javax.inject.Inject
@@ -30,7 +32,6 @@ import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
class NotificationRenderer @Inject constructor(
- private val notificationIdProvider: NotificationIdProvider,
private val notificationDisplayer: NotificationDisplayer,
private val notificationDataFactory: NotificationDataFactory,
) {
@@ -59,14 +60,14 @@ class NotificationRenderer @Inject constructor(
Timber.tag(loggerTag.value).d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage(
tag = null,
- id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
+ id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
)
}
roomNotifications.forEach { notificationData ->
notificationDisplayer.showNotificationMessage(
tag = notificationData.roomId.value,
- id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
+ id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
@@ -76,7 +77,7 @@ class NotificationRenderer @Inject constructor(
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = notificationData.key,
- id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
+ id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
@@ -87,7 +88,7 @@ class NotificationRenderer @Inject constructor(
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
notificationDisplayer.showNotificationMessage(
tag = notificationData.key,
- id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
+ id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
notification = notificationData.notification
)
}
@@ -98,7 +99,7 @@ class NotificationRenderer @Inject constructor(
Timber.tag(loggerTag.value).d("Showing fallback notification")
notificationDisplayer.showNotificationMessage(
tag = "FALLBACK",
- id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
+ id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
notification = fallbackNotifications.first().notification
)
}
@@ -108,7 +109,7 @@ class NotificationRenderer @Inject constructor(
Timber.tag(loggerTag.value).d("Updating summary notification")
notificationDisplayer.showNotificationMessage(
tag = null,
- id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
+ id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
notification = summaryNotification.notification
)
}
@@ -127,6 +128,8 @@ private fun List.groupByType(): GroupedNotificationEvents {
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
is FallbackNotifiableEvent -> fallbackEvents.add(event.castedToEventType())
+ // Nothing should be done for ringing call events as they're not handled here
+ is NotifiableRingingCallEvent -> {}
}
}
return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents)
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 3285a7ae90..9fcbf8c152 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
@@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
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 8c74f845b7..a40871be4c 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
@@ -19,10 +19,15 @@ 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
+import android.media.RingtoneManager
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.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
@@ -30,15 +35,51 @@ import io.element.android.libraries.push.impl.R
import io.element.android.services.toolbox.api.strings.StringProvider
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 RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID"
+
/**
* on devices >= android O, we need to define a channel for each notifications.
*/
+interface NotificationChannels {
+ /**
+ * Get the channel for incoming call.
+ * @param ring true if the device should ring when receiving the call.
+ */
+ fun getChannelForIncomingCall(ring: Boolean): String
+
+ /**
+ * Get the channel for messages.
+ * @param noisy true if the notification should have sound and vibration.
+ */
+ fun getChannelIdForMessage(noisy: Boolean): String
+
+ /**
+ * Get the channel for test notifications.
+ */
+ fun getChannelIdForTest(): String
+}
+
+@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
+private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+
@SingleIn(AppScope::class)
-class NotificationChannels @Inject constructor(
+@ContributesBinding(AppScope::class)
+class DefaultNotificationChannels @Inject constructor(
@ApplicationContext private val context: Context,
private val notificationManager: NotificationManagerCompat,
private val stringProvider: StringProvider,
-) {
+) : NotificationChannels {
init {
createNotificationChannels()
}
@@ -75,6 +116,9 @@ class NotificationChannels @Inject constructor(
}
}
+ // Migration - Create new call channel
+ notificationManager.deleteNotificationChannel(CALL_NOTIFICATION_CHANNEL_ID_V2)
+
/**
* Default notification importance: shows everywhere, makes noise, but does not visually
* intrude.
@@ -123,46 +167,52 @@ class NotificationChannels @Inject constructor(
}
)
+ // Register a channel for incoming and in progress call notifications with no ringing
notificationManager.createNotificationChannel(
NotificationChannel(
- CALL_NOTIFICATION_CHANNEL_ID,
+ CALL_NOTIFICATION_CHANNEL_ID_V3,
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
NotificationManager.IMPORTANCE_HIGH
)
.apply {
description = stringProvider.getString(R.string.notification_channel_call)
- setSound(null, null)
+ enableVibration(true)
enableLights(true)
lightColor = accentColor
}
)
+
+ // Register a channel for incoming call notifications which will ring the device when received
+ val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
+ notificationManager.createNotificationChannel(
+ NotificationChannelCompat.Builder(
+ RINGING_CALL_NOTIFICATION_CHANNEL_ID,
+ NotificationManagerCompat.IMPORTANCE_MAX,
+ )
+ .setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" })
+ .setVibrationEnabled(true)
+ .setSound(
+ ringtoneUri,
+ AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setLegacyStreamType(AudioManager.STREAM_RING)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build()
+ )
+ .setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls))
+ .setLightsEnabled(true)
+ .setLightColor(accentColor)
+ .build()
+ )
}
- private fun getChannel(channelId: String): NotificationChannel? {
- return notificationManager.getNotificationChannel(channelId)
+ override fun getChannelForIncomingCall(ring: Boolean): String {
+ return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3
}
- 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 {
+ override 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
- }
+ override fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
index cd6b32086b..5c8dd1dc77 100755
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt
@@ -35,9 +35,10 @@ import io.element.android.libraries.di.AppScope
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.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.R
-import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
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.debug.annotateForDebug
@@ -129,12 +130,16 @@ class DefaultNotificationCreator @Inject constructor(
val smallIcon = CommonDrawables.ic_notification_small
- val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
+ val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY }
+ val channelId = if (containsMissedCall) {
+ notificationChannels.getChannelForIncomingCall(false)
+ } else {
+ notificationChannels.getChannelIdForMessage(noisy = roomInfo.shouldBing)
+ }
val builder = if (existingNotification != null) {
NotificationCompat.Builder(context, existingNotification)
} else {
NotificationCompat.Builder(context, channelId)
- .setOnlyAlertOnce(roomInfo.isUpdated)
// 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)
@@ -210,6 +215,11 @@ class DefaultNotificationCreator @Inject constructor(
setLargeIcon(largeIcon)
}
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
+
+ // If any of the events are of call notify type it means a missed call, set the category to the right value
+ if (events.any { it.type == EventType.CALL_NOTIFY }) {
+ setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
+ }
}
.setTicker(tickerText)
.build()
@@ -343,7 +353,6 @@ class DefaultNotificationCreator @Inject constructor(
.setWhen(lastMessageTimestamp)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
- // set content text to support devices running API level < 24
.setGroup(currentUser.userId.value)
// set this notification as the summary for the group
.setGroupSummary(true)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
index 015e24d5e6..4f3dbf57b3 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
@@ -51,9 +51,9 @@ data class NotifiableMessageEvent(
val outGoingMessage: Boolean = false,
val outGoingMessageFailed: Boolean = false,
override val isRedacted: Boolean = false,
- override val isUpdated: Boolean = false
-) : NotifiableEvent {
+ override val isUpdated: Boolean = false,
val type: String = EventType.MESSAGE
+) : NotifiableEvent {
override val description: String = body ?: ""
// Example of value:
@@ -69,9 +69,16 @@ fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationSta
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
null -> false
- else -> appNavigationState.isInForeground &&
- sessionId == currentSessionId &&
- roomId == currentRoomId &&
- (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
+ else -> {
+ // Never ignore ringing call notifications
+ if (this is NotifiableRingingCallEvent) {
+ false
+ } else {
+ appNavigationState.isInForeground &&
+ sessionId == currentSessionId &&
+ roomId == currentRoomId &&
+ (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
+ }
+ }
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt
new file mode 100644
index 0000000000..0b9696177d
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableRingingCallEvent.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2024 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.model
+
+import io.element.android.libraries.matrix.api.core.EventId
+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.UserId
+import io.element.android.libraries.matrix.api.notification.CallNotifyType
+import java.time.Instant
+import kotlin.time.Duration.Companion.seconds
+
+data class NotifiableRingingCallEvent(
+ override val sessionId: SessionId,
+ override val roomId: RoomId,
+ override val eventId: EventId,
+ override val editedEventId: EventId?,
+ override val description: String?,
+ override val canBeReplaced: Boolean,
+ override val isRedacted: Boolean,
+ override val isUpdated: Boolean,
+ val roomName: String?,
+ val senderId: UserId,
+ val senderDisambiguatedDisplayName: String?,
+ val senderAvatarUrl: String?,
+ val roomAvatarUrl: String? = null,
+ val callNotifyType: CallNotifyType,
+ val timestamp: Long,
+) : NotifiableEvent {
+ companion object {
+ fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean {
+ val timeout = 10.seconds.inWholeMilliseconds
+ val elapsed = Instant.now().toEpochMilli() - timestamp
+ // Only ring if the type is RING and the elapsed time is less than the timeout
+ return callNotifyType == CallNotifyType.RING && elapsed < timeout
+ }
+ }
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
index 1d3f349364..fda14f294f 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
@@ -17,11 +17,15 @@
package io.element.android.libraries.push.impl.push
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
+import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
+import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
@@ -44,6 +48,8 @@ class DefaultPushHandler @Inject constructor(
private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val diagnosticPushHandler: DiagnosticPushHandler,
+ private val elementCallEntryPoint: ElementCallEntryPoint,
+ private val notificationChannels: NotificationChannels,
) : PushHandler {
/**
* Called when message is received.
@@ -91,19 +97,33 @@ class DefaultPushHandler @Inject constructor(
return
}
val userPushStore = userPushStoreFactory.getOrCreate(userId)
- if (userPushStore.getNotificationEnabledForDevice().first()) {
+ val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
+ if (areNotificationsEnabled) {
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
- if (notifiableEvent == null) {
- Timber.w("Unable to get a notification data")
- return
+ when (notifiableEvent) {
+ null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
+ is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
+ else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
}
- onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
- // TODO We need to check if this is an incoming call
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
}
}
+
+ private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
+ Timber.i("## handleInternal() : Incoming call.")
+ elementCallEntryPoint.handleIncomingCall(
+ callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId),
+ eventId = notifiableEvent.eventId,
+ senderId = notifiableEvent.senderId,
+ roomName = notifiableEvent.roomName,
+ senderName = notifiableEvent.senderDisambiguatedDisplayName,
+ avatarUrl = notifiableEvent.roomAvatarUrl,
+ timestamp = notifiableEvent.timestamp,
+ notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true),
+ )
+ }
}
diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml
index 1064d5c31e..e1bfa07d29 100644
--- a/libraries/push/impl/src/main/res/values/localazy.xml
+++ b/libraries/push/impl/src/main/res/values/localazy.xml
@@ -3,6 +3,7 @@
"Call"
"Listening for events"
"Noisy notifications"
+ "Ringing calls"
"Silent notifications"
- "%1$s: %2$d message"
@@ -13,6 +14,7 @@
- "%d notifications"
"Notification"
+ "Incoming call"
"** Failed to send - please open room"
"Join"
"Reject"
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt
index 4f715ed283..70403e1986 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt
@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
@@ -33,6 +34,8 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultActiveNotificationsProviderTest {
+ private val notificationIdProvider = NotificationIdProvider
+
@Test
fun `getAllNotifications with no active notifications returns empty list`() {
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
@@ -43,7 +46,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getAllNotifications with active notifications returns all`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
@@ -57,7 +59,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getNotificationsForSession returns only notifications for that session id`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
@@ -71,7 +72,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
@@ -89,7 +89,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
@@ -117,7 +116,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
@@ -145,7 +143,6 @@ class DefaultActiveNotificationsProviderTest {
@Test
fun `getSummaryNotification returns only the summary notification for that session id if it exists`() {
- val notificationIdProvider = NotificationIdProvider()
val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
@@ -172,7 +169,6 @@ class DefaultActiveNotificationsProviderTest {
}
return DefaultActiveNotificationsProvider(
notificationManager = notificationManager,
- notificationIdProvider = NotificationIdProvider(),
)
}
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt
index 8eebbbbb5c..afcdc90b9d 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt
@@ -18,12 +18,15 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context
import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
@@ -48,7 +51,9 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
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.NotifiableRingingCallEvent
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
+import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import kotlinx.coroutines.test.runTest
@@ -58,6 +63,7 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
+@Suppress("LargeClass")
@RunWith(RobolectricTestRunner::class)
class DefaultNotifiableEventResolverTest {
@Test
@@ -479,6 +485,109 @@ class DefaultNotifiableEventResolverTest {
assertThat(result).isEqualTo(expectedResult)
}
+ @Test
+ fun `resolve CallNotify - ringing`() = runTest {
+ val timestamp = DefaultSystemClock().epochMillis()
+ val sut = createDefaultNotifiableEventResolver(
+ notificationResult = Result.success(
+ createNotificationData(
+ content = NotificationContent.MessageLike.CallNotify(
+ A_USER_ID_2,
+ CallNotifyType.RING
+ ),
+ timestamp = timestamp,
+ )
+ )
+ )
+ val expectedResult = NotifiableRingingCallEvent(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ eventId = AN_EVENT_ID,
+ senderId = A_USER_ID_2,
+ roomName = null,
+ editedEventId = null,
+ description = "Incoming call",
+ timestamp = timestamp,
+ canBeReplaced = true,
+ isRedacted = false,
+ isUpdated = false,
+ senderDisambiguatedDisplayName = "Bob",
+ senderAvatarUrl = null,
+ callNotifyType = CallNotifyType.RING,
+ )
+ val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+ assertThat(result).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun `resolve CallNotify - ring but timed out displays the same as notify`() = runTest {
+ val sut = createDefaultNotifiableEventResolver(
+ notificationResult = Result.success(
+ createNotificationData(
+ content = NotificationContent.MessageLike.CallNotify(
+ A_USER_ID_2,
+ CallNotifyType.RING
+ ),
+ timestamp = 0L,
+ )
+ )
+ )
+ val expectedResult = NotifiableMessageEvent(
+ sessionId = A_SESSION_ID,
+ eventId = AN_EVENT_ID,
+ editedEventId = null,
+ noisy = true,
+ timestamp = 0L,
+ senderDisambiguatedDisplayName = "Bob",
+ senderId = UserId("@bob:server.org"),
+ body = "☎\uFE0F Incoming call",
+ roomId = A_ROOM_ID,
+ threadId = null,
+ roomName = null,
+ roomIsDirect = false,
+ canBeReplaced = false,
+ isRedacted = false,
+ imageUriString = null,
+ type = EventType.CALL_NOTIFY,
+ )
+ val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+ assertThat(result).isEqualTo(expectedResult)
+ }
+
+ @Test
+ fun `resolve CallNotify - notify`() = runTest {
+ val sut = createDefaultNotifiableEventResolver(
+ notificationResult = Result.success(
+ createNotificationData(
+ content = NotificationContent.MessageLike.CallNotify(
+ A_USER_ID_2,
+ CallNotifyType.NOTIFY
+ )
+ )
+ )
+ )
+ val expectedResult = NotifiableMessageEvent(
+ sessionId = A_SESSION_ID,
+ eventId = AN_EVENT_ID,
+ editedEventId = null,
+ noisy = true,
+ timestamp = A_TIMESTAMP,
+ senderDisambiguatedDisplayName = "Bob",
+ senderId = UserId("@bob:server.org"),
+ body = "☎\uFE0F Incoming call",
+ roomId = A_ROOM_ID,
+ threadId = null,
+ roomName = null,
+ roomIsDirect = false,
+ canBeReplaced = false,
+ isRedacted = false,
+ imageUriString = null,
+ type = EventType.CALL_NOTIFY,
+ )
+ val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
+ assertThat(result).isEqualTo(expectedResult)
+ }
+
@Test
fun `resolve null cases`() {
testNull(NotificationContent.MessageLike.CallAnswer)
@@ -558,6 +667,7 @@ class DefaultNotifiableEventResolverTest {
content: NotificationContent,
isDirect: Boolean = false,
hasMention: Boolean = false,
+ timestamp: Long = A_TIMESTAMP,
): NotificationData {
return NotificationData(
eventId = AN_EVENT_ID,
@@ -570,7 +680,7 @@ class DefaultNotifiableEventResolverTest {
isDirect = isDirect,
isEncrypted = false,
isNoisy = false,
- timestamp = A_TIMESTAMP,
+ timestamp = timestamp,
content = content,
hasMention = hasMention,
)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
index 3924171d60..bbe5e273b2 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt
@@ -28,12 +28,13 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
-import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
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
+import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.NavigationState
@@ -164,7 +165,7 @@ class DefaultNotificationDrawerManagerTest {
val notificationManager = mockk {
every { cancel(any(), any()) } returns Unit
}
- val summaryId = NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)
+ val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
val activeNotificationsProvider = FakeActiveNotificationsProvider(
mutableListOf(
mockk {
@@ -198,7 +199,6 @@ class DefaultNotificationDrawerManagerTest {
return DefaultNotificationDrawerManager(
notificationManager = notificationManager,
notificationRenderer = NotificationRenderer(
- notificationIdProvider = NotificationIdProvider(),
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)),
notificationDataFactory = DefaultNotificationDataFactory(
notificationCreator = FakeNotificationCreator(),
@@ -208,7 +208,6 @@ class DefaultNotificationDrawerManagerTest {
stringProvider = FakeStringProvider(),
),
),
- notificationIdProvider = NotificationIdProvider(),
appNavigationStateService = appNavigationStateService,
coroutineScope = this,
matrixClientProvider = matrixClientProvider,
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt
new file mode 100644
index 0000000000..ce2a698ae1
--- /dev/null
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024 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
+
+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.matrix.test.FakeMatrixClientProvider
+import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
+import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory
+import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
+import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
+import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
+import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.mockk.mockk
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DefaultOnMissedCallNotificationHandlerTest {
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `addMissedCallNotification - should add missed call notification`() = runTest {
+ val childScope = CoroutineScope(coroutineContext + SupervisorJob())
+ val dataFactory = FakeNotificationDataFactory(
+ messageEventToNotificationsResult = lambdaRecorder { _, _, _ -> emptyList() }
+ )
+ val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler(
+ defaultNotificationDrawerManager = DefaultNotificationDrawerManager(
+ notificationManager = mockk(relaxed = true),
+ notificationRenderer = NotificationRenderer(
+ notificationDisplayer = FakeNotificationDisplayer(),
+ notificationDataFactory = dataFactory,
+ ),
+ appNavigationStateService = FakeAppNavigationStateService(),
+ coroutineScope = childScope,
+ matrixClientProvider = FakeMatrixClientProvider(),
+ imageLoaderHolder = FakeImageLoaderHolder(),
+ activeNotificationsProvider = FakeActiveNotificationsProvider(),
+ ),
+ notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent() }),
+ )
+
+ defaultOnMissedCallNotificationHandler.addMissedCallNotification(
+ sessionId = A_SESSION_ID,
+ roomId = A_ROOM_ID,
+ eventId = AN_EVENT_ID,
+ )
+
+ runCurrent()
+
+ dataFactory.messageEventToNotificationsResult.assertions().isCalledOnce()
+
+ // Cancel the coroutine scope so the test can finish
+ childScope.cancel()
+ }
+}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
index 8a8b5efd43..2b5b941292 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultRoomGroupMessageCreatorTest.kt
@@ -25,8 +25,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
-import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
+import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
@@ -212,7 +212,7 @@ fun createRoomGroupMessageCreator(
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
): RoomGroupMessageCreator {
val context = RuntimeEnvironment.getApplication() as Context
- val bitmapLoader = NotificationBitmapLoader(
+ val bitmapLoader = DefaultNotificationBitmapLoader(
context = RuntimeEnvironment.getApplication(),
sdkIntProvider = sdkIntProvider,
)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt
index aff5de7d4b..27f66a7b33 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt
@@ -23,13 +23,13 @@ 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.FakeActiveNotificationsProvider
-import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
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
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
+import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -37,6 +37,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
private val MY_AVATAR_URL: String? = null
+
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
index b9664ef577..17f736df86 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
@@ -19,12 +19,13 @@ package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import org.junit.Test
class NotificationIdProviderTest {
@Test
fun `test notification id provider`() {
- val sut = NotificationIdProvider()
+ val sut = NotificationIdProvider
val offsetForASessionId = 305_410
assertThat(sut.getSummaryNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 0)
assertThat(sut.getRoomMessagesNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 1)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
index 0a5b216ea0..e83c5d2351 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
@@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
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.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
-import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
@@ -31,6 +31,7 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
+import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@@ -61,10 +62,9 @@ class NotificationRendererTest {
activeNotificationsProvider = FakeActiveNotificationsProvider(),
stringProvider = FakeStringProvider(),
)
- private val notificationIdProvider = NotificationIdProvider()
+ private val notificationIdProvider = NotificationIdProvider
private val notificationRenderer = NotificationRenderer(
- notificationIdProvider = notificationIdProvider,
notificationDisplayer = notificationDisplayer,
notificationDataFactory = notificationDataFactory,
)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt
new file mode 100644
index 0000000000..9b5f8de3be
--- /dev/null
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 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
+
+class FakeNotificationChannels(
+ var channelForIncomingCall: (ring: Boolean) -> String = { _ -> "" },
+ var channelIdForMessage: (noisy: Boolean) -> String = { _ -> "" },
+ var channelIdForTest: () -> String = { "" }
+) : NotificationChannels {
+ override fun getChannelForIncomingCall(ring: Boolean): String {
+ return channelForIncomingCall(ring)
+ }
+
+ override fun getChannelIdForMessage(noisy: Boolean): String {
+ return channelIdForMessage(noisy)
+ }
+
+ override fun getChannelIdForTest(): String {
+ return channelIdForTest()
+ }
+}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt
new file mode 100644
index 0000000000..ec87069cd2
--- /dev/null
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2024 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.NotificationChannel
+import android.os.Build
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import io.element.android.services.toolbox.test.strings.FakeStringProvider
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+class NotificationChannelsTest {
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `init - creates notification channels and migrates old ones`() {
+ val notificationManager = mockk(relaxed = true) {
+ every { notificationChannels } returns emptyList()
+ }
+
+ createNotificationChannels(notificationManager = notificationManager)
+
+ verify { notificationManager.createNotificationChannel(any()) }
+ verify { notificationManager.createNotificationChannel(any()) }
+ verify { notificationManager.deleteNotificationChannel(any()) }
+ }
+
+ @Test
+ fun `getChannelForIncomingCall - returns the right channel`() {
+ val notificationChannels = createNotificationChannels()
+
+ val ringingChannel = notificationChannels.getChannelForIncomingCall(ring = true)
+ assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID)
+
+ val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false)
+ assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID_V3)
+ }
+
+ @Test
+ fun `getChannelIdForMessage - returns the right channel`() {
+ val notificationChannels = createNotificationChannels()
+
+ assertThat(notificationChannels.getChannelIdForMessage(noisy = true)).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID)
+ assertThat(notificationChannels.getChannelIdForMessage(noisy = false)).isEqualTo(SILENT_NOTIFICATION_CHANNEL_ID)
+ }
+
+ @Test
+ fun `getChannelIdForTest - returns the right channel`() {
+ val notificationChannels = createNotificationChannels()
+
+ assertThat(notificationChannels.getChannelIdForTest()).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID)
+ }
+
+ private fun createNotificationChannels(
+ notificationManager: NotificationManagerCompat = mockk(relaxed = true),
+ ) = DefaultNotificationChannels(
+ context = InstrumentationRegistry.getInstrumentation().targetContext,
+ notificationManager = notificationManager,
+ stringProvider = FakeStringProvider(),
+ )
+}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
index de0c22d1a6..124c71adcf 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt
@@ -29,18 +29,20 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
+import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
-import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
+import io.element.android.libraries.push.impl.notifications.channels.DefaultNotificationChannels
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.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
+import io.element.android.libraries.push.test.notifications.FakeImageLoader
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
@@ -284,7 +286,7 @@ fun createNotificationCreator(
context: Context = RuntimeEnvironment.getApplication(),
buildMeta: BuildMeta = aBuildMeta(),
notificationChannels: NotificationChannels = createNotificationChannels(),
- bitmapLoader: NotificationBitmapLoader = NotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
+ bitmapLoader: NotificationBitmapLoader = DefaultNotificationBitmapLoader(context, FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.R)),
): NotificationCreator {
return DefaultNotificationCreator(
context = context,
@@ -327,5 +329,5 @@ fun createNotificationCreator(
fun createNotificationChannels(): NotificationChannels {
val context = RuntimeEnvironment.getApplication()
- return NotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider(""))
+ return DefaultNotificationChannels(context, NotificationManagerCompat.from(context), FakeStringProvider(""))
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt
index 221d3d0878..a35ee45996 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt
@@ -46,7 +46,7 @@ class FakeNotificationDataFactory(
var inviteToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() },
var simpleEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() },
var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> =
- lambdaRecorder { _ -> emptyList() },
+ lambdaRecorder { _ -> emptyList() },
) : NotificationDataFactory {
override suspend fun toNotifications(messages: List, currentUser: MatrixUser, imageLoader: ImageLoader): List {
return messageEventToNotificationsResult(messages, currentUser, imageLoader)
@@ -64,6 +64,8 @@ class FakeNotificationDataFactory(
return simpleEventToNotificationsResult(simpleEvents)
}
+ @JvmName("toNotificationFallbackEvents")
+ @Suppress("INAPPLICABLE_JVM_NAME")
override fun toNotifications(fallback: List): List {
return fallbackEventToNotificationsResult(fallback)
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt
index c8c041720c..3ce472b02d 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt
@@ -18,8 +18,8 @@ package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
-import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
@@ -51,7 +51,7 @@ class FakeNotificationDisplayer(
fun verifySummaryCancelled(times: Int = 1) {
cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence(
- listOf(value(null), value(NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)))
+ listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)))
)
}
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
index 096ae254b8..cd2c66178f 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
@@ -21,11 +21,16 @@ 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.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.notification.CallNotifyType
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
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.matrix.test.A_USER_ID_2
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.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
fun aSimpleNotifiableEvent(
@@ -79,6 +84,7 @@ fun aNotifiableMessageEvent(
threadId: ThreadId? = null,
isRedacted: Boolean = false,
timestamp: Long = 0,
+ type: String = EventType.MESSAGE,
) = NotifiableMessageEvent(
sessionId = sessionId,
eventId = eventId,
@@ -94,5 +100,34 @@ fun aNotifiableMessageEvent(
roomIsDirect = false,
canBeReplaced = false,
isRedacted = isRedacted,
- imageUriString = null
+ imageUriString = null,
+ type = type,
+)
+
+fun anNotifiableCallEvent(
+ sessionId: SessionId = A_SESSION_ID,
+ roomId: RoomId = A_ROOM_ID,
+ eventId: EventId = AN_EVENT_ID,
+ senderId: UserId = A_USER_ID_2,
+ senderName: String? = null,
+ roomAvatarUrl: String? = AN_AVATAR_URL,
+ senderAvatarUrl: String? = AN_AVATAR_URL,
+ callNotifyType: CallNotifyType = CallNotifyType.NOTIFY,
+ timestamp: Long = 0L,
+) = NotifiableRingingCallEvent(
+ sessionId = sessionId,
+ eventId = eventId,
+ roomId = roomId,
+ roomName = "a room name",
+ editedEventId = null,
+ description = "description",
+ timestamp = timestamp,
+ canBeReplaced = false,
+ isRedacted = false,
+ isUpdated = false,
+ senderDisambiguatedDisplayName = senderName,
+ senderId = senderId,
+ roomAvatarUrl = roomAvatarUrl,
+ senderAvatarUrl = senderAvatarUrl,
+ callNotifyType = callNotifyType,
)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
index 7739efbd1d..9205a72530 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt
@@ -19,11 +19,16 @@
package io.element.android.libraries.push.impl.push
import app.cash.turbine.test
+import io.element.android.features.call.api.CallType
+import io.element.android.features.call.test.FakeElementCallEntryPoint
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.EventId
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.UserId
+import io.element.android.libraries.matrix.api.notification.CallNotifyType
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
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_SECRET
@@ -31,7 +36,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
+import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
+import io.element.android.libraries.push.impl.notifications.fixtures.anNotifiableCallEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
@@ -47,6 +54,7 @@ import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
+import java.time.Instant
class DefaultPushHandlerTest {
@Test
@@ -220,6 +228,55 @@ class DefaultPushHandlerTest {
.isNeverCalled()
}
+ @Test
+ fun `when ringing call PushData is received, the incoming call will be handled`() = runTest {
+ val aPushData = PushData(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ unread = 0,
+ clientSecret = A_SECRET,
+ )
+ val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> }
+ val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
+ val defaultPushHandler = createDefaultPushHandler(
+ elementCallEntryPoint = elementCallEntryPoint,
+ notifiableEventResult = { _, _, _ -> anNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
+ incrementPushCounterResult = {},
+ pushClientSecret = FakePushClientSecret(
+ getUserIdFromSecretResult = { A_USER_ID }
+ ),
+ )
+ defaultPushHandler.handle(aPushData)
+
+ handleIncomingCallLambda.assertions().isCalledOnce()
+ }
+
+ @Test
+ fun `when notify call PushData is received, the incoming call will be treated as a normal notification`() = runTest {
+ val aPushData = PushData(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ unread = 0,
+ clientSecret = A_SECRET,
+ )
+ val onNotifiableEventReceived = lambdaRecorder {}
+ val handleIncomingCallLambda = lambdaRecorder { _, _, _, _, _, _, _ -> }
+ val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
+ val defaultPushHandler = createDefaultPushHandler(
+ elementCallEntryPoint = elementCallEntryPoint,
+ onNotifiableEventReceived = onNotifiableEventReceived,
+ notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent(type = EventType.CALL_NOTIFY) },
+ incrementPushCounterResult = {},
+ pushClientSecret = FakePushClientSecret(
+ getUserIdFromSecretResult = { A_USER_ID }
+ ),
+ )
+ defaultPushHandler.handle(aPushData)
+
+ handleIncomingCallLambda.assertions().isNeverCalled()
+ onNotifiableEventReceived.assertions().isCalledOnce()
+ }
+
@Test
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() =
runTest {
@@ -249,6 +306,8 @@ class DefaultPushHandlerTest {
buildMeta: BuildMeta = aBuildMeta(),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
+ elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
+ notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
): DefaultPushHandler {
return DefaultPushHandler(
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
@@ -263,6 +322,8 @@ class DefaultPushHandlerTest {
buildMeta = buildMeta,
matrixAuthenticationService = matrixAuthenticationService,
diagnosticPushHandler = diagnosticPushHandler,
+ elementCallEntryPoint = elementCallEntryPoint,
+ notificationChannels = notificationChannels,
)
}
}
diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts
index 7826c3072b..be8acf362c 100644
--- a/libraries/push/test/build.gradle.kts
+++ b/libraries/push/test/build.gradle.kts
@@ -24,7 +24,13 @@ android {
dependencies {
api(projects.libraries.push.api)
+ implementation(projects.libraries.push.impl)
implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
implementation(projects.libraries.pushproviders.api)
implementation(projects.tests.testutils)
+ implementation(libs.androidx.core)
+ implementation(libs.coil.compose)
+ implementation(libs.coil.test)
+ implementation(libs.test.robolectric)
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt
similarity index 96%
rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt
rename to libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt
index 5c747b45e2..7fd8945e91 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoader.kt
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.libraries.push.impl.notifications.fake
+package io.element.android.libraries.push.test.notifications
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt
similarity index 90%
rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt
rename to libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt
index 0e18036e94..57b71007e4 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeImageLoaderHolder.kt
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023 New Vector Ltd
+ * Copyright (c) 2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.libraries.push.impl.notifications.fake
+package io.element.android.libraries.push.test.notifications
import coil.ImageLoader
import io.element.android.libraries.matrix.api.MatrixClient
diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt
new file mode 100644
index 0000000000..1803be9a0a
--- /dev/null
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeOnMissedCallNotificationHandler.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 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.test.notifications
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
+
+class FakeOnMissedCallNotificationHandler(
+ var addMissedCallNotificationLambda: (SessionId, RoomId, EventId) -> Unit = { _, _, _ -> }
+) : OnMissedCallNotificationHandler {
+ override suspend fun addMissedCallNotification(
+ sessionId: SessionId,
+ roomId: RoomId,
+ eventId: EventId,
+ ) {
+ addMissedCallNotificationLambda(sessionId, roomId, eventId)
+ }
+}
diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt
new file mode 100644
index 0000000000..e21c409ccb
--- /dev/null
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/push/FakeNotificationBitmapLoader.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 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.test.notifications.push
+
+import android.graphics.Bitmap
+import androidx.core.graphics.drawable.IconCompat
+import coil.ImageLoader
+import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
+
+class FakeNotificationBitmapLoader(
+ var getRoomBitmapResult: (String?, ImageLoader) -> Bitmap? = { _, _ -> null },
+ var getUserIconResult: (String?, ImageLoader) -> IconCompat? = { _, _ -> null },
+) : NotificationBitmapLoader {
+ override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
+ return getRoomBitmapResult(path, imageLoader)
+ }
+
+ override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
+ return getUserIconResult(path, imageLoader)
+ }
+}
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index a106be8b5a..e856789944 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -83,6 +83,7 @@
"Quick reply"
"Quote"
"React"
+ "Reject"
"Remove"
"Reply"
"Reply in thread"
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt
index b227918461..18285488e0 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt
@@ -85,6 +85,13 @@ inline fun lambdaRecorder(
+ ensureNeverCalled: Boolean = false,
+ noinline block: (T1, T2, T3, T4, T5, T6, T7) -> R
+): LambdaSevenParamsRecorder {
+ return LambdaSevenParamsRecorder(ensureNeverCalled, block)
+}
+
inline fun lambdaAnyRecorder(
ensureNeverCalled: Boolean = false,
noinline block: (List) -> R
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..85e6cc4f6f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Day-1_2_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52ba9c146f9ceee768b971bee87c7db41321684c77727669acf38e9c32698f96
+size 65711
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..6920a3fd65
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_IncomingCallScreen_null_IncomingCallScreen-Night-1_3_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d4019a436d20847db4e2e2b35f96d143790056b867d40dcdc93fe4af33c7f77
+size 59021
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
index 5e56497e00..9c9bcd90e5 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:22b40382bcb7f296e2b944b954298692c2946693d185976a6b128ec221d1ac5b
-size 59235
+oid sha256:d3d1bbc0c03ac483d0047bc4711eb63741c2071f013ee5962a1184e6112bef0c
+size 60386
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
index 0e86234e07..5205d00d66 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2e5e3a60c51736f2c8e293fccc01cfa969525be86f6f500ffd22ee24413cb659
-size 57864
+oid sha256:b2162a49d87b23c25251c6a8c322ec62eee2eb34e802ab5ce2ecacb637735554
+size 59189
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png
index 257534fa92..4462ff8798 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_10,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:130c300ec896d60e665cd3b11b3d58dd72b54e01365053a677330a52b86314c7
-size 18532
+oid sha256:8b20a53d7a1bb9e6cdb5dbae068c1934d4716986618596b33899caa975070f41
+size 14802
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png
index 37040e69bc..4e2e4c0254 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_11,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a
-size 21480
+oid sha256:f0ce8e6b948f953ebfb183939e606f3faa0d73ad35c2707b9821f8229bce37e2
+size 19071
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png
index 5aa2ad2c6f..44a9f12406 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_12,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:90fbc995b03553699a0ea3cfca65b3c0d800ad91f7405cd59d0f6110aeaa3783
-size 17591
+oid sha256:a1c284262da7cfc1af8aa583d2762084bf1693c44f83bf1cc1de2d74f57dae0e
+size 19408
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png
index 53d55e5152..257534fa92 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_13,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f0d092bc75f8199660f9e527f2712824535ddbcde91da6cefe4b306f668dc026
-size 16279
+oid sha256:130c300ec896d60e665cd3b11b3d58dd72b54e01365053a677330a52b86314c7
+size 18532
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png
index eab29dd2b2..37040e69bc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_14,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f
-size 20933
+oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a
+size 21480
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png
index bf6ccb2dd9..5aa2ad2c6f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_15,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:999fc081f03ee5e80036278cd80098cabfd0b776362ac871c4324b2c1c48bf97
-size 19232
+oid sha256:90fbc995b03553699a0ea3cfca65b3c0d800ad91f7405cd59d0f6110aeaa3783
+size 17591
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png
index 258b1f2c09..53d55e5152 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_16,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:be7d7c2ad722d9b3b3ca4f9ec4994df387c32fa28480bb8ef2b0d9b57bfcc588
-size 16994
+oid sha256:f0d092bc75f8199660f9e527f2712824535ddbcde91da6cefe4b306f668dc026
+size 16279
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png
index 714308dfa4..eab29dd2b2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_17,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168
-size 24574
+oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f
+size 20933
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png
index bde780a065..bf6ccb2dd9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_18,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dffbd73e81a83bec2d11604411d7153bcda8ec44b1e5edc96ad5efd347441137
-size 14509
+oid sha256:999fc081f03ee5e80036278cd80098cabfd0b776362ac871c4324b2c1c48bf97
+size 19232
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png
index 6856025214..258b1f2c09 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_19,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e925ecead3a108881093e83a15fa2702ece8bdb950336489def58c860960e177
-size 13651
+oid sha256:be7d7c2ad722d9b3b3ca4f9ec4994df387c32fa28480bb8ef2b0d9b57bfcc588
+size 16994
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png
index 39197031a7..714308dfa4 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_20,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5fd5a514986ca09fd5d59f2ef6c1061510ea22a40ff74ce3a5048464368bdbbe
-size 16583
+oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168
+size 24574
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png
index d013a9b536..bde780a065 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_21,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5424c7ac0dccbcbf2768d129ec31d2675a729ee17852f1c773afeebf9540f3a2
-size 17349
+oid sha256:dffbd73e81a83bec2d11604411d7153bcda8ec44b1e5edc96ad5efd347441137
+size 14509
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png
index fb83908f7b..6856025214 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_22,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec91c4129a04a5bbd82add9216657d1ef6f61c62cdda8e7969d62a9d3d11b03a
-size 16037
+oid sha256:e925ecead3a108881093e83a15fa2702ece8bdb950336489def58c860960e177
+size 13651
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png
index 95efeca161..39197031a7 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_23,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fbb735a2e27619514a06ebc6e1d84fb6a37d1447a46797e68302dc76655bf323
-size 20700
+oid sha256:5fd5a514986ca09fd5d59f2ef6c1061510ea22a40ff74ce3a5048464368bdbbe
+size 16583
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png
index a183374ac6..d013a9b536 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_24,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56b2d5b052303c44434c9752a3d2fcd2c40453370a17c24c2ebf33cc777a7e7b
-size 17761
+oid sha256:5424c7ac0dccbcbf2768d129ec31d2675a729ee17852f1c773afeebf9540f3a2
+size 17349
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png
index a9bdcd2885..fb83908f7b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_25,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b09c2ececc08eb3caa70afd3dbd8fd6b34a8d64eb9d9dc99e5ebb1ea2d8d96ea
-size 16456
+oid sha256:ec91c4129a04a5bbd82add9216657d1ef6f61c62cdda8e7969d62a9d3d11b03a
+size 16037
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png
index 261dd54d80..95efeca161 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_26,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b84949b7a5256c5f3881b9811705eb2f6f2c7b108fb85aa45adf2d4c8afd2b7f
-size 21127
+oid sha256:fbb735a2e27619514a06ebc6e1d84fb6a37d1447a46797e68302dc76655bf323
+size 20700
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png
index fa5d08e9a2..a183374ac6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_27,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:06310ae4063a59045b5da45b811b9cb29bd483beb86cce8262494177d3925e0f
-size 14583
+oid sha256:56b2d5b052303c44434c9752a3d2fcd2c40453370a17c24c2ebf33cc777a7e7b
+size 17761
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png
index 90d3edbcb6..a9bdcd2885 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_28,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db4f833beff2bb588fccc1294e76703ff64d427bbf141cf730538a856992d609
-size 13808
+oid sha256:b09c2ececc08eb3caa70afd3dbd8fd6b34a8d64eb9d9dc99e5ebb1ea2d8d96ea
+size 16456
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png
index 25f01d0c77..261dd54d80 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_29,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:be37247d044b78541c1da7d4e28a3906e89a369f978a486ae1c1aa70b583658f
-size 16321
+oid sha256:b84949b7a5256c5f3881b9811705eb2f6f2c7b108fb85aa45adf2d4c8afd2b7f
+size 21127
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png
index 1ae28cd8b1..8f54e4f0be 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:29b4f035a161707d8c551f70478651b1c49136489f282f56c1c10af399e182b9
-size 19853
+oid sha256:134390dcc21a7131a88b8060d15ae4a830dd058b1b89518264dd58f503c8b81d
+size 22547
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png
index 7caaf879aa..fa5d08e9a2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_30,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:85db59158254fa043e53feeddce69cfe89a308a385dde3dcd9a6ff6b94aef7c2
-size 15105
+oid sha256:06310ae4063a59045b5da45b811b9cb29bd483beb86cce8262494177d3925e0f
+size 14583
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png
index 60077980a3..90d3edbcb6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_31,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d22ae88ce675b24306df9d598e78600ccb31e203b30f5cfc9e94432bedc518bd
-size 14324
+oid sha256:db4f833beff2bb588fccc1294e76703ff64d427bbf141cf730538a856992d609
+size 13808
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png
index 21606b47a9..25f01d0c77 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_32,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7
-size 16836
+oid sha256:be37247d044b78541c1da7d4e28a3906e89a369f978a486ae1c1aa70b583658f
+size 16321
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png
index eec3b821df..7caaf879aa 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_33,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f0d524d6f2a6f0cb2b06e8a359c66819dc7e8eb03ad0c17612d35f002f2189f9
-size 15484
+oid sha256:85db59158254fa043e53feeddce69cfe89a308a385dde3dcd9a6ff6b94aef7c2
+size 15105
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png
index cf8459e6ba..60077980a3 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_34,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ebe050aae72443fdfb7a8cb11e7b5d89a0614907155c3bbebda656c8d44b30a1
-size 15141
+oid sha256:d22ae88ce675b24306df9d598e78600ccb31e203b30f5cfc9e94432bedc518bd
+size 14324
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png
index 881a35582b..21606b47a9 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_35,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4
-size 16371
+oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7
+size 16836
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png
index e96833b111..eec3b821df 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_36,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77d4c098189f99f7091738791218ae663d168d30a442804b8923243c8727598b
-size 16145
+oid sha256:f0d524d6f2a6f0cb2b06e8a359c66819dc7e8eb03ad0c17612d35f002f2189f9
+size 15484
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png
index 1478eba397..cf8459e6ba 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_37,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:74da4791ade57bbf0b0393143c3f40e9c8aaec36f438ecc85a872ad0f0e03da3
-size 15388
+oid sha256:ebe050aae72443fdfb7a8cb11e7b5d89a0614907155c3bbebda656c8d44b30a1
+size 15141
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png
index ee7f8a89cc..881a35582b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_38,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf
-size 17891
+oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4
+size 16371
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png
index fcaf26295f..e96833b111 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_39,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ee072668c6161aa3e2195c2f963bb502112bf6ade57a35be44b013b30aa3d0a9
-size 19308
+oid sha256:77d4c098189f99f7091738791218ae663d168d30a442804b8923243c8727598b
+size 16145
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png
index 8a3b5ced60..99f1aa1671 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5fd0e375d985f2e77eea87650455c7bc5b4e8f1f5f58467811f686316c5449c9
-size 17581
+oid sha256:4d8ee1ebdce2f3024b4f813afd80732afbaff74168d4d7af826e4c5dc1714d15
+size 19159
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png
index b63064c734..1478eba397 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_40,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:06d03aa5e848872228a58b399685bc78dfdbad783cb28bfed609dcdc371a168f
-size 18520
+oid sha256:74da4791ade57bbf0b0393143c3f40e9c8aaec36f438ecc85a872ad0f0e03da3
+size 15388
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png
index 83586a0c51..ee7f8a89cc 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_41,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98
-size 21073
+oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf
+size 17891
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png
index 598a31fd16..fcaf26295f 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_42,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f96f83d866d29d3eb713adf9fb8e216414a1eb924a63d0458c77522ba0de0499
-size 16709
+oid sha256:ee072668c6161aa3e2195c2f963bb502112bf6ade57a35be44b013b30aa3d0a9
+size 19308
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png
index 0e609afb69..b63064c734 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_43,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93e866bf58f381020d9516662332a6c986bfd61acc9954723b27adb52c152d68
-size 15473
+oid sha256:06d03aa5e848872228a58b399685bc78dfdbad783cb28bfed609dcdc371a168f
+size 18520
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png
index 3bef3f861b..83586a0c51 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_44,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f27e368c8c68032dc4eac63012fad459785c42c4063aca99eb6be9d8abe99508
-size 19751
+oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98
+size 21073
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png
index cbc5a4ba10..598a31fd16 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_45,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1e531de46bbc667647a29dc40e8751bad96a089d6765fcddfc69c47147602563
-size 12918
+oid sha256:f96f83d866d29d3eb713adf9fb8e216414a1eb924a63d0458c77522ba0de0499
+size 16709
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png
index 1d1884a7ac..0e609afb69 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_46,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bc57d1ccd6657417469d0d76de066f3a555a5296a83b1cb7168cd64680f97bcd
-size 12584
+oid sha256:93e866bf58f381020d9516662332a6c986bfd61acc9954723b27adb52c152d68
+size 15473
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png
index b42d0c123d..3bef3f861b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_47,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0960f771d8158f52ad925c04c3d9770f4babec0d9ce017958ccd813b4c0012bd
-size 13796
+oid sha256:f27e368c8c68032dc4eac63012fad459785c42c4063aca99eb6be9d8abe99508
+size 19751
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png
index 488c8a32d2..cbc5a4ba10 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fe2a0b40572b28fb5934d999ce4fecdf76f7efb61caf390669054c084af594ad
-size 18778
+oid sha256:1e531de46bbc667647a29dc40e8751bad96a089d6765fcddfc69c47147602563
+size 12918
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png
index a59d5f174c..1d1884a7ac 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:57358f0990a41b7d22a42b5f06662a8549de79a50126491fa9518bc3ce33bf9a
-size 17099
+oid sha256:bc57d1ccd6657417469d0d76de066f3a555a5296a83b1cb7168cd64680f97bcd
+size 12584
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png
index 18efb9a84b..8100c9c10c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f
-size 25028
+oid sha256:038e9a9b4ef1536e0f77d8330935a8ad6bf2c1664d8d0362fcfca29eb2b3200d
+size 30081
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png
index e941cef17e..b42d0c123d 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a5aab04563cc94c4a7636402def1be7d738dd182f733de390d1ad028e99e332
-size 22831
+oid sha256:0960f771d8158f52ad925c04c3d9770f4babec0d9ce017958ccd813b4c0012bd
+size 13796
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png
index 69af73d4be..488c8a32d2 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_51,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:16b2c55f72419639690b09ef46c85c1d284d1da79439adacb38775ae6352c3fb
-size 21125
+oid sha256:fe2a0b40572b28fb5934d999ce4fecdf76f7efb61caf390669054c084af594ad
+size 18778
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png
index 696beb67c6..a59d5f174c 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_52,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a86f83a07283d0c98c64d3a041a6d0b285991f64a34ad372f07d2e04e4d0eff6
-size 19455
+oid sha256:57358f0990a41b7d22a42b5f06662a8549de79a50126491fa9518bc3ce33bf9a
+size 17099
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png
index ad3bbe7088..e941cef17e 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_53,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c6202d80bb0a938bdfca6afef92fc71a00863d2e2c119f62fc61ede3df47cf7b
-size 24925
+oid sha256:5a5aab04563cc94c4a7636402def1be7d738dd182f733de390d1ad028e99e332
+size 22831
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png
index c670583b4b..69af73d4be 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_54,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7eb42896831203a1cd8f1ac85b96d33d1048d626f5ee4778733a28d48aeebea4
-size 16744
+oid sha256:16b2c55f72419639690b09ef46c85c1d284d1da79439adacb38775ae6352c3fb
+size 21125
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png
index 6f8e06f572..696beb67c6 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_55,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93f23803bd0b5633615fb7c1cb40c76a7a1a755f43fb755a08cb390dfd956d75
-size 15967
+oid sha256:a86f83a07283d0c98c64d3a041a6d0b285991f64a34ad372f07d2e04e4d0eff6
+size 19455
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png
index dd81493a76..ad3bbe7088 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_56,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201
-size 18491
+oid sha256:c6202d80bb0a938bdfca6afef92fc71a00863d2e2c119f62fc61ede3df47cf7b
+size 24925
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png
index 3e1cb98594..c670583b4b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_57,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f600185bb7a9fe6f4e425d8bf43b2743e6169f3a13c34117059aaa863c1b1de0
-size 21593
+oid sha256:7eb42896831203a1cd8f1ac85b96d33d1048d626f5ee4778733a28d48aeebea4
+size 16744
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png
index 5a6d361d40..6f8e06f572 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2559b3b0e5242e9867aa5c821551a17dbab723e1018476289cb91ed9b9f9ffc4
-size 20736
+oid sha256:93f23803bd0b5633615fb7c1cb40c76a7a1a755f43fb755a08cb390dfd956d75
+size 15967
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png
index c1712835ff..dd81493a76 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482
-size 23624
+oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201
+size 18491
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png
index 2740280540..1ae28cd8b1 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:69dcd8a997542d56ad3c327f3937f9fdc31eecebe30a07a2e6ee26e631c140e6
-size 16047
+oid sha256:29b4f035a161707d8c551f70478651b1c49136489f282f56c1c10af399e182b9
+size 19853
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png
index 821589afc5..3e1cb98594 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e70ce111262f22f14ada0993a32d23e1532c6d7a24f71c38d6aba4af6610054
-size 17350
+oid sha256:f600185bb7a9fe6f4e425d8bf43b2743e6169f3a13c34117059aaa863c1b1de0
+size 21593
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png
index d0b966d6ec..5a6d361d40 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:88fc583b69d153e9027a2df3f98b45a607dcf32838af033b82b3b94e3e03dcc2
-size 16486
+oid sha256:2559b3b0e5242e9867aa5c821551a17dbab723e1018476289cb91ed9b9f9ffc4
+size 20736
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png
index 0b8c15e54c..c1712835ff 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c
-size 19436
+oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482
+size 23624
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..821589afc5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_63,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3e70ce111262f22f14ada0993a32d23e1532c6d7a24f71c38d6aba4af6610054
+size 17350
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d0b966d6ec
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_64,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:88fc583b69d153e9027a2df3f98b45a607dcf32838af033b82b3b94e3e03dcc2
+size 16486
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..0b8c15e54c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_65,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c
+size 19436
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png
index 4462ff8798..8a3b5ced60 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8b20a53d7a1bb9e6cdb5dbae068c1934d4716986618596b33899caa975070f41
-size 14802
+oid sha256:5fd0e375d985f2e77eea87650455c7bc5b4e8f1f5f58467811f686316c5449c9
+size 17581
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png
index 4e2e4c0254..18efb9a84b 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f0ce8e6b948f953ebfb183939e606f3faa0d73ad35c2707b9821f8229bce37e2
-size 19071
+oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f
+size 25028
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png
index 44a9f12406..2740280540 100644
--- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a1c284262da7cfc1af8aa583d2762084bf1693c44f83bf1cc1de2d74f57dae0e
-size 19408
+oid sha256:69dcd8a997542d56ad3c327f3937f9fdc31eecebe30a07a2e6ee26e631c140e6
+size 16047
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index d23a72f7a0..1e9281e8c3 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -236,9 +236,10 @@
]
},
{
- "name" : ":features:call",
+ "name" : ":features:call:impl",
"includeRegex" : [
- "call_.*"
+ "call_.*",
+ "screen_incoming_call.*"
]
},
{