Element Call ringing notifications (#2978)
- Add `ActiveCallManager` to handle incoming and ongoing calls. - Add ringing call notifications with full screen intents and missed call ones as part of the 'conversation' notifications. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
b827b859dc
commit
6f8de0b2c6
186 changed files with 2686 additions and 330 deletions
|
|
@ -17,5 +17,13 @@
|
||||||
package io.element.android.appconfig
|
package io.element.android.appconfig
|
||||||
|
|
||||||
object ElementCallConfig {
|
object ElementCallConfig {
|
||||||
|
/**
|
||||||
|
* The default base URL for the Element Call service.
|
||||||
|
*/
|
||||||
const val DEFAULT_BASE_URL = "https://call.element.io"
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/2894.feature
Normal file
1
changelog.d/2894.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Ringing call notifications and full screen ringing screen for DMs when the device is locked.
|
||||||
31
features/call/api/build.gradle.kts
Normal file
31
features/call/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.call
|
package io.element.android.features.call.api
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -23,11 +23,15 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.call"
|
namespace = "io.element.android.features.call.impl"
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -41,12 +45,17 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.matrix.impl)
|
implementation(projects.libraries.matrix.impl)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.libraries.push.api)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(libs.androidx.webkit)
|
implementation(libs.androidx.webkit)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
|
api(projects.features.call.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
@ -54,9 +63,12 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
|
testImplementation(libs.test.mockk)
|
||||||
|
testImplementation(projects.features.call.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|
@ -23,11 +23,17 @@
|
||||||
android:name="android.hardware.microphone"
|
android:name="android.hardware.microphone"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<!-- Permissions for call foreground services -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
|
|
@ -70,10 +76,24 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".ui.IncomingCallActivity"
|
||||||
|
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="io.element.android.features.call" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".CallForegroundService"
|
android:name=".services.CallForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="phoneCall" />
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||||
|
android:exported="false"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -14,13 +14,17 @@
|
||||||
* limitations under the License.
|
* 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 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
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
interface CallBindings {
|
interface CallBindings {
|
||||||
fun inject(callActivity: ElementCallActivity)
|
fun inject(callActivity: ElementCallActivity)
|
||||||
|
fun inject(callActivity: IncomingCallActivity)
|
||||||
|
fun inject(declineCallBroadcastReceiver: DeclineCallBroadcastReceiver)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<CallBindings>().inject(this)
|
||||||
|
activeCallManager.hungUpCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,30 +14,36 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.call
|
package io.element.android.features.call.impl.services
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.PendingIntentCompat
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
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.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() {
|
class CallForegroundService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
val intent = Intent(context, CallForegroundService::class.java)
|
val intent = Intent(context, CallForegroundService::class.java)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
ContextCompat.startForegroundService(context, intent)
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
|
|
@ -69,7 +75,17 @@ class CallForegroundService : Service() {
|
||||||
.setContentText(getString(R.string.call_foreground_service_message_android))
|
.setContentText(getString(R.string.call_foreground_service_message_android))
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.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() {
|
override fun onDestroy() {
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
* limitations under the License.
|
* 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 {
|
sealed interface CallScreenEvents {
|
||||||
data object Hangup : CallScreenEvents
|
data object Hangup : CallScreenEvents
|
||||||
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
|
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) :
|
||||||
|
CallScreenEvents
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
@ -30,11 +30,12 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
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.data.WidgetMessage
|
import io.element.android.features.call.impl.data.WidgetMessage
|
||||||
import io.element.android.features.call.utils.CallWidgetProvider
|
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||||
import io.element.android.features.call.utils.WidgetMessageInterceptor
|
import io.element.android.features.call.impl.utils.CallWidgetProvider
|
||||||
import io.element.android.features.call.utils.WidgetMessageSerializer
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -65,6 +66,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val screenTracker: ScreenTracker,
|
private val screenTracker: ScreenTracker,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val activeCallManager: ActiveCallManager,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -84,6 +86,10 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
loadUrl(callType, urlState, callWidgetDriver)
|
loadUrl(callType, urlState, callWidgetDriver)
|
||||||
|
|
||||||
|
if (callType is CallType.RoomCall) {
|
||||||
|
activeCallManager.joinedCall(callType.sessionId, callType.roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (callType) {
|
when (callType) {
|
||||||
|
|
@ -134,6 +140,14 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
if (callType is CallType.RoomCall) {
|
||||||
|
activeCallManager.hungUpCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: CallScreenEvents) {
|
fun handleEvents(event: CallScreenEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is CallScreenEvents.Hangup -> {
|
is CallScreenEvents.Hangup -> {
|
||||||
|
|
@ -193,7 +207,6 @@ class CallScreenPresenter @AssistedInject constructor(
|
||||||
val client = (callType as? CallType.RoomCall)?.sessionId?.let {
|
val client = (callType as? CallType.RoomCall)?.sessionId?.let {
|
||||||
matrixClientsProvider.getOrNull(it)
|
matrixClientsProvider.getOrNull(it)
|
||||||
} ?: return@DisposableEffect onDispose { }
|
} ?: return@DisposableEffect onDispose { }
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
client.syncService().syncState
|
client.syncService().syncState
|
||||||
.onEach { state ->
|
.onEach { state ->
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.annotation.SuppressLint
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
|
@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.call.R
|
import io.element.android.features.call.impl.R
|
||||||
import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor
|
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -14,12 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.call.ui
|
package io.element.android.features.call.impl.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
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.Theme
|
||||||
import io.element.android.compound.theme.isDark
|
import io.element.android.compound.theme.isDark
|
||||||
import io.element.android.compound.theme.mapToTheme
|
import io.element.android.compound.theme.mapToTheme
|
||||||
import io.element.android.features.call.CallForegroundService
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||||
import io.element.android.features.call.di.CallBindings
|
import io.element.android.features.call.impl.di.CallBindings
|
||||||
import io.element.android.features.call.utils.CallIntentDataParser
|
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.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
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 callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
|
@ -88,7 +72,13 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
applicationContext.bindings<CallBindings>().inject(this)
|
applicationContext.bindings<CallBindings>().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)
|
setCallType(intent)
|
||||||
|
|
||||||
|
|
@ -157,16 +147,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCallType(intent: Intent?) {
|
private fun setCallType(intent: Intent?) {
|
||||||
val inputs = intent?.let {
|
val callType = intent?.let {
|
||||||
IntentCompat.getParcelableExtra(it, EXTRA_CALL_WIDGET_SETTINGS, CallType::class.java)
|
IntentCompat.getParcelableExtra(it, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallType::class.java)
|
||||||
}
|
}
|
||||||
val intentUrl = intent?.dataString?.let(::parseUrl)
|
val intentUrl = intent?.dataString?.let(::parseUrl)
|
||||||
when {
|
when {
|
||||||
// Re-opened the activity but we have no url to load or a cached one, finish the activity
|
// 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()
|
intent?.dataString == null && callType == null && webViewTarget.value == null -> finish()
|
||||||
inputs != null -> {
|
callType != null -> {
|
||||||
webViewTarget.value = inputs
|
webViewTarget.value = callType
|
||||||
presenter = presenterFactory.create(inputs, this)
|
presenter = presenterFactory.create(callType, this)
|
||||||
}
|
}
|
||||||
intentUrl != null -> {
|
intentUrl != null -> {
|
||||||
val fallbackInputs = CallType.ExternalUrl(intentUrl)
|
val fallbackInputs = CallType.ExternalUrl(intentUrl)
|
||||||
|
|
@ -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<CallBindings>().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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ActiveCall?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<ActiveCall?>(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
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
import io.element.android.appconfig.ElementCallConfig
|
||||||
|
|
@ -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
|
||||||
|
)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.graphics.Bitmap
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
|
|
@ -22,7 +22,7 @@ import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.webkit.WebViewCompat
|
import androidx.webkit.WebViewCompat
|
||||||
import androidx.webkit.WebViewFeature
|
import androidx.webkit.WebViewFeature
|
||||||
import io.element.android.features.call.BuildConfig
|
import io.element.android.features.call.impl.BuildConfig
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
class WebViewWidgetMessageInterceptor(
|
class WebViewWidgetMessageInterceptor(
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* 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
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
object WidgetMessageSerializer {
|
object WidgetMessageSerializer {
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
|
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
|
||||||
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
|
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
|
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Incoming Element Call"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -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<CallNotificationData, Unit> {}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.call
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
import com.google.common.truth.Truth.assertThat
|
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
|
import org.junit.Test
|
||||||
|
|
||||||
class MapWebkitPermissionsTest {
|
class MapWebkitPermissionsTest {
|
||||||
|
|
@ -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<String?, ImageLoader, IconCompat?> { _, _ -> 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,11 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
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.FakeCallWidgetProvider
|
||||||
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
|
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -254,6 +258,7 @@ class CallScreenPresenterTest {
|
||||||
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
|
||||||
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
|
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||||
screenTracker: ScreenTracker = FakeScreenTracker(),
|
screenTracker: ScreenTracker = FakeScreenTracker(),
|
||||||
): CallScreenPresenter {
|
): CallScreenPresenter {
|
||||||
val userAgentProvider = object : UserAgentProvider {
|
val userAgentProvider = object : UserAgentProvider {
|
||||||
|
|
@ -270,8 +275,9 @@ class CallScreenPresenterTest {
|
||||||
clock = clock,
|
clock = clock,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
matrixClientsProvider = matrixClientsProvider,
|
matrixClientsProvider = matrixClientsProvider,
|
||||||
screenTracker = screenTracker,
|
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
|
activeCallManager = activeCallManager,
|
||||||
|
screenTracker = screenTracker,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.call.ui
|
package io.element.android.features.call.ui
|
||||||
|
|
||||||
|
import io.element.android.features.call.impl.ui.CallScreenNavigator
|
||||||
|
|
||||||
class FakeCallScreenNavigator : CallScreenNavigator {
|
class FakeCallScreenNavigator : CallScreenNavigator {
|
||||||
var closeCalled = false
|
var closeCalled = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
@ -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<NotificationManagerCompat>(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<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
|
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<NotificationManagerCompat>(relaxed = true)
|
||||||
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
|
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<NotificationManagerCompat>(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<NotificationManagerCompat>(relaxed = true)
|
||||||
|
val sendCallNotifyLambda = lambdaRecorder<Result<Unit>> { 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
|
|
@ -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<ActiveCall?>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.call.utils
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.utils
|
||||||
|
|
||||||
|
import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
class FakeWidgetMessageInterceptor : WidgetMessageInterceptor {
|
class FakeWidgetMessageInterceptor : WidgetMessageInterceptor {
|
||||||
34
features/call/test/build.gradle.kts
Normal file
34
features/call/test/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ dependencies {
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.messages.api)
|
api(projects.features.messages.api)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call.api)
|
||||||
implementation(projects.features.location.api)
|
implementation(projects.features.location.api)
|
||||||
implementation(projects.features.poll.api)
|
implementation(projects.features.poll.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -31,8 +30,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.ui.ElementCallActivity
|
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
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.inputs
|
||||||
import io.element.android.libraries.architecture.overlay.Overlay
|
import io.element.android.libraries.architecture.overlay.Overlay
|
||||||
import io.element.android.libraries.architecture.overlay.operation.show
|
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.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -78,11 +76,11 @@ import kotlinx.parcelize.Parcelize
|
||||||
class MessagesFlowNode @AssistedInject constructor(
|
class MessagesFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -188,12 +186,12 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onJoinCallClick(roomId: RoomId) {
|
override fun onJoinCallClick(roomId: RoomId) {
|
||||||
val inputs = CallType.RoomCall(
|
val callType = CallType.RoomCall(
|
||||||
sessionId = matrixClient.sessionId,
|
sessionId = matrixClient.sessionId,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
)
|
)
|
||||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||||
ElementCallActivity.start(context, inputs)
|
elementCallEntryPoint.startCall(callType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inputs = MessagesNode.Inputs(
|
val inputs = MessagesNode.Inputs(
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ class TimelineControllerTest {
|
||||||
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
|
matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline))
|
||||||
val sut = TimelineController(matrixRoom)
|
val sut = TimelineController(matrixRoom)
|
||||||
sut.activeTimelineFlow().test {
|
sut.activeTimelineFlow().test {
|
||||||
|
sut.focusOnEvent(AN_EVENT_ID)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state).isEqualTo(liveTimeline)
|
assertThat(state).isEqualTo(liveTimeline)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ dependencies {
|
||||||
api(projects.libraries.usersearch.api)
|
api(projects.libraries.usersearch.api)
|
||||||
api(projects.services.apperror.api)
|
api(projects.services.apperror.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call.api)
|
||||||
implementation(projects.features.createroom.api)
|
implementation(projects.features.createroom.api)
|
||||||
implementation(projects.features.leaveroom.api)
|
implementation(projects.features.leaveroom.api)
|
||||||
implementation(projects.features.userprofile.shared)
|
implementation(projects.features.userprofile.shared)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl
|
package io.element.android.features.roomdetails.impl
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -30,8 +29,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.ui.ElementCallActivity
|
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
|
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.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
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.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -62,8 +60,8 @@ import kotlinx.parcelize.Parcelize
|
||||||
class RoomDetailsFlowNode @AssistedInject constructor(
|
class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
|
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
|
||||||
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||||
|
|
@ -147,7 +145,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
)
|
)
|
||||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
|
||||||
ElementCallActivity.start(context, inputs)
|
elementCallEntryPoint.startCall(inputs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||||
|
|
@ -195,7 +193,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCall(dmRoomId: RoomId) {
|
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)
|
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,10 @@ private fun Content(desktopApplicationName: String) {
|
||||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
|
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
|
||||||
add(
|
add(
|
||||||
annotatedTextWithBold(
|
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)
|
boldText = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.mediaviewer.api)
|
implementation(projects.libraries.mediaviewer.api)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call.api)
|
||||||
api(projects.features.userprofile.api)
|
api(projects.features.userprofile.api)
|
||||||
api(projects.features.userprofile.shared)
|
api(projects.features.userprofile.shared)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.userprofile.impl
|
package io.element.android.features.userprofile.impl
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -29,8 +28,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.ui.ElementCallActivity
|
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
import io.element.android.features.userprofile.api.UserProfileEntryPoint
|
||||||
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
import io.element.android.features.userprofile.impl.root.UserProfileNode
|
||||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
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.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
|
@ -53,7 +51,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class UserProfileFlowNode @AssistedInject constructor(
|
class UserProfileFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
@ApplicationContext private val context: Context,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val sessionIdHolder: CurrentSessionIdHolder,
|
private val sessionIdHolder: CurrentSessionIdHolder,
|
||||||
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -84,7 +82,7 @@ class UserProfileFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCall(dmRoomId: RoomId) {
|
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<UserProfileEntryPoint.Params>().userId)
|
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||||
enum class AvatarSize(val dp: Dp) {
|
enum class AvatarSize(val dp: Dp) {
|
||||||
CurrentUserTopBar(32.dp),
|
CurrentUserTopBar(32.dp),
|
||||||
|
|
||||||
|
IncomingCall(140.dp),
|
||||||
RoomHeader(96.dp),
|
RoomHeader(96.dp),
|
||||||
RoomListItem(52.dp),
|
RoomListItem(52.dp),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -332,5 +332,10 @@ interface MatrixRoom : Closeable {
|
||||||
*/
|
*/
|
||||||
suspend fun getPermalinkFor(eventId: EventId): Result<String>
|
suspend fun getPermalinkFor(eventId: EventId): Result<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an Element Call started notification if needed.
|
||||||
|
*/
|
||||||
|
suspend fun sendCallNotificationIfNeeded(): Result<Unit>
|
||||||
|
|
||||||
override fun close() = destroy()
|
override fun close() = destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ object EventType {
|
||||||
const val CALL_NEGOTIATE = "m.call.negotiate"
|
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||||
const val CALL_REJECT = "m.call.reject"
|
const val CALL_REJECT = "m.call.reject"
|
||||||
const val CALL_HANGUP = "m.call.hangup"
|
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
|
// This type is not processed by the client, just sent to the server
|
||||||
const val CALL_REPLACES = "m.call.replaces"
|
const val CALL_REPLACES = "m.call.replaces"
|
||||||
|
|
@ -94,6 +95,7 @@ object EventType {
|
||||||
type == CALL_SELECT_ANSWER ||
|
type == CALL_SELECT_ANSWER ||
|
||||||
type == CALL_NEGOTIATE ||
|
type == CALL_NEGOTIATE ||
|
||||||
type == CALL_REJECT ||
|
type == CALL_REJECT ||
|
||||||
type == CALL_REPLACES
|
type == CALL_REPLACES ||
|
||||||
|
type == CALL_NOTIFY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,10 @@ class RustMatrixRoom(
|
||||||
innerRoom.matrixToEventPermalink(eventId.value)
|
innerRoom.matrixToEventPermalink(eventId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> = runCatching {
|
||||||
|
innerRoom.sendCallNotificationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
private fun createTimeline(
|
private fun createTimeline(
|
||||||
timeline: InnerTimeline,
|
timeline: InnerTimeline,
|
||||||
isLive: Boolean,
|
isLive: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ class FakeMatrixRoom(
|
||||||
override val liveTimeline: Timeline = FakeTimeline(),
|
override val liveTimeline: Timeline = FakeTimeline(),
|
||||||
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
|
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
|
||||||
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
|
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
|
||||||
|
var sendCallNotificationIfNeededResult: () -> Result<Unit> = { Result.success(Unit) },
|
||||||
canRedactOwn: Boolean = false,
|
canRedactOwn: Boolean = false,
|
||||||
canRedactOther: Boolean = false,
|
canRedactOther: Boolean = false,
|
||||||
) : MatrixRoom {
|
) : MatrixRoom {
|
||||||
|
|
@ -528,6 +529,10 @@ class FakeMatrixRoom(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<String> = generateWidgetWebViewUrlResult
|
): Result<String> = generateWidgetWebViewUrlResult
|
||||||
|
|
||||||
|
override suspend fun sendCallNotificationIfNeeded(): Result<Unit> {
|
||||||
|
return sendCallNotificationIfNeededResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||||
|
|
||||||
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
fun givenRoomMembersState(state: MatrixRoomMembersState) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -14,13 +14,12 @@
|
||||||
* limitations under the License.
|
* 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 io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class NotificationIdProvider @Inject constructor() {
|
object NotificationIdProvider {
|
||||||
fun getSummaryNotificationId(sessionId: SessionId): Int {
|
fun getSummaryNotificationId(sessionId: SessionId): Int {
|
||||||
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
|
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
|
||||||
}
|
}
|
||||||
|
|
@ -41,16 +40,30 @@ class NotificationIdProvider @Inject constructor() {
|
||||||
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
|
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 {
|
private fun getOffset(sessionId: SessionId): Int {
|
||||||
// Compute a int from a string with a low risk of collision.
|
// Compute a int from a string with a low risk of collision.
|
||||||
return abs(sessionId.value.hashCode() % 100_000) * 10
|
return abs(sessionId.value.hashCode() % 100_000) * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private const val FALLBACK_NOTIFICATION_ID = -1
|
||||||
private const val FALLBACK_NOTIFICATION_ID = -1
|
private const val SUMMARY_NOTIFICATION_ID = 0
|
||||||
private const val SUMMARY_NOTIFICATION_ID = 0
|
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||||
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),
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,7 @@ dependencies {
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.troubleshoot.api)
|
implementation(projects.libraries.troubleshoot.api)
|
||||||
|
implementation(projects.features.call.api)
|
||||||
api(projects.libraries.pushproviders.api)
|
api(projects.libraries.pushproviders.api)
|
||||||
api(projects.libraries.pushstore.api)
|
api(projects.libraries.pushstore.api)
|
||||||
api(projects.libraries.push.api)
|
api(projects.libraries.push.api)
|
||||||
|
|
@ -76,6 +77,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.libraries.pushstore.test)
|
testImplementation(projects.libraries.pushstore.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(projects.features.call.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.toolbox.impl)
|
testImplementation(projects.services.toolbox.impl)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.SessionId
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface ActiveNotificationsProvider {
|
interface ActiveNotificationsProvider {
|
||||||
|
|
@ -37,7 +38,6 @@ interface ActiveNotificationsProvider {
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultActiveNotificationsProvider @Inject constructor(
|
class DefaultActiveNotificationsProvider @Inject constructor(
|
||||||
private val notificationManager: NotificationManagerCompat,
|
private val notificationManager: NotificationManagerCompat,
|
||||||
private val notificationIdProvider: NotificationIdProvider,
|
|
||||||
) : ActiveNotificationsProvider {
|
) : ActiveNotificationsProvider {
|
||||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||||
return notificationManager.activeNotifications
|
return notificationManager.activeNotifications
|
||||||
|
|
@ -48,22 +48,22 @@ class DefaultActiveNotificationsProvider @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
|
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||||
val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
val notificationId = NotificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||||
val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId)
|
val summaryId = NotificationIdProvider.getSummaryNotificationId(sessionId)
|
||||||
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
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.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.FileMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
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.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
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.NotifiableMessageEvent
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
@ -150,8 +152,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
NotificationContent.MessageLike.CallAnswer,
|
NotificationContent.MessageLike.CallAnswer,
|
||||||
NotificationContent.MessageLike.CallCandidates,
|
NotificationContent.MessageLike.CallCandidates,
|
||||||
NotificationContent.MessageLike.CallHangup,
|
NotificationContent.MessageLike.CallHangup -> {
|
||||||
is NotificationContent.MessageLike.CallNotify -> { // TODO CallNotify will be handled separately in the future
|
|
||||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +173,44 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
||||||
senderAvatarPath = senderAvatarUrl,
|
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.KeyVerificationAccept,
|
||||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||||
NotificationContent.MessageLike.KeyVerificationDone,
|
NotificationContent.MessageLike.KeyVerificationDone,
|
||||||
|
|
@ -334,7 +373,8 @@ private fun buildNotifiableMessageEvent(
|
||||||
outGoingMessage: Boolean = false,
|
outGoingMessage: Boolean = false,
|
||||||
outGoingMessageFailed: Boolean = false,
|
outGoingMessageFailed: Boolean = false,
|
||||||
isRedacted: Boolean = false,
|
isRedacted: Boolean = false,
|
||||||
isUpdated: Boolean = false
|
isUpdated: Boolean = false,
|
||||||
|
type: String = EventType.MESSAGE,
|
||||||
) = NotifiableMessageEvent(
|
) = NotifiableMessageEvent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
senderId = senderId,
|
senderId = senderId,
|
||||||
|
|
@ -356,5 +396,6 @@ private fun buildNotifiableMessageEvent(
|
||||||
outGoingMessage = outGoingMessage,
|
outGoingMessage = outGoingMessage,
|
||||||
outGoingMessageFailed = outGoingMessageFailed,
|
outGoingMessageFailed = outGoingMessageFailed,
|
||||||
isRedacted = isRedacted,
|
isRedacted = isRedacted,
|
||||||
isUpdated = isUpdated
|
isUpdated = isUpdated,
|
||||||
|
type = type,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,27 @@ import androidx.core.graphics.drawable.toBitmap
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transform.CircleCropTransformation
|
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.di.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
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 io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NotificationBitmapLoader @Inject constructor(
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultNotificationBitmapLoader @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) {
|
) : NotificationBitmapLoader {
|
||||||
/**
|
/**
|
||||||
* Get icon of a room.
|
* Get icon of a room.
|
||||||
* @param path mxc url
|
* @param path mxc url
|
||||||
* @param imageLoader Coil image loader
|
* @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) {
|
if (path == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +71,7 @@ class NotificationBitmapLoader @Inject constructor(
|
||||||
* @param path mxc url
|
* @param path mxc url
|
||||||
* @param imageLoader Coil image loader
|
* @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) {
|
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -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.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
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.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.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
|
|
@ -53,7 +54,6 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.
|
||||||
class DefaultNotificationDrawerManager @Inject constructor(
|
class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
private val notificationManager: NotificationManagerCompat,
|
private val notificationManager: NotificationManagerCompat,
|
||||||
private val notificationRenderer: NotificationRenderer,
|
private val notificationRenderer: NotificationRenderer,
|
||||||
private val notificationIdProvider: NotificationIdProvider,
|
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
|
@ -124,7 +124,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
* Clear all known message events for a [sessionId].
|
* Clear all known message events for a [sessionId].
|
||||||
*/
|
*/
|
||||||
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||||
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||||
clearSummaryNotificationIfNeeded(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.
|
* Can also be called when a notification for this room is dismissed by the user.
|
||||||
*/
|
*/
|
||||||
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +165,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
||||||
* Clear the notifications for a single event.
|
* Clear the notifications for a single event.
|
||||||
*/
|
*/
|
||||||
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||||
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
val id = NotificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||||
notificationManager.cancel(eventId.value, id)
|
notificationManager.cancel(eventId.value, id)
|
||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,8 @@ interface NotificationDataFactory {
|
||||||
@JvmName("toNotificationSimpleEvents")
|
@JvmName("toNotificationSimpleEvents")
|
||||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
|
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
|
||||||
|
@JvmName("toNotificationFallbackEvents")
|
||||||
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
|
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
|
||||||
|
|
||||||
fun createSummaryNotification(
|
fun createSummaryNotification(
|
||||||
|
|
@ -130,6 +132,8 @@ class DefaultNotificationDataFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmName("toNotificationFallbackEvents")
|
||||||
|
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||||
return fallback.map { event ->
|
return fallback.map { event ->
|
||||||
OneShotNotification(
|
OneShotNotification(
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,12 @@ package io.element.android.libraries.push.impl.notifications
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
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.FallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
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.NotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
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 io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -30,7 +32,6 @@ import javax.inject.Inject
|
||||||
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
|
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
|
||||||
|
|
||||||
class NotificationRenderer @Inject constructor(
|
class NotificationRenderer @Inject constructor(
|
||||||
private val notificationIdProvider: NotificationIdProvider,
|
|
||||||
private val notificationDisplayer: NotificationDisplayer,
|
private val notificationDisplayer: NotificationDisplayer,
|
||||||
private val notificationDataFactory: NotificationDataFactory,
|
private val notificationDataFactory: NotificationDataFactory,
|
||||||
) {
|
) {
|
||||||
|
|
@ -59,14 +60,14 @@ class NotificationRenderer @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).d("Removing summary notification")
|
Timber.tag(loggerTag.value).d("Removing summary notification")
|
||||||
notificationDisplayer.cancelNotificationMessage(
|
notificationDisplayer.cancelNotificationMessage(
|
||||||
tag = null,
|
tag = null,
|
||||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
roomNotifications.forEach { notificationData ->
|
roomNotifications.forEach { notificationData ->
|
||||||
notificationDisplayer.showNotificationMessage(
|
notificationDisplayer.showNotificationMessage(
|
||||||
tag = notificationData.roomId.value,
|
tag = notificationData.roomId.value,
|
||||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||||
notification = notificationData.notification
|
notification = notificationData.notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +77,7 @@ class NotificationRenderer @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||||
notificationDisplayer.showNotificationMessage(
|
notificationDisplayer.showNotificationMessage(
|
||||||
tag = notificationData.key,
|
tag = notificationData.key,
|
||||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||||
notification = notificationData.notification
|
notification = notificationData.notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +88,7 @@ class NotificationRenderer @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||||
notificationDisplayer.showNotificationMessage(
|
notificationDisplayer.showNotificationMessage(
|
||||||
tag = notificationData.key,
|
tag = notificationData.key,
|
||||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||||
notification = notificationData.notification
|
notification = notificationData.notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +99,7 @@ class NotificationRenderer @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||||
notificationDisplayer.showNotificationMessage(
|
notificationDisplayer.showNotificationMessage(
|
||||||
tag = "FALLBACK",
|
tag = "FALLBACK",
|
||||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||||
notification = fallbackNotifications.first().notification
|
notification = fallbackNotifications.first().notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +109,7 @@ class NotificationRenderer @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).d("Updating summary notification")
|
Timber.tag(loggerTag.value).d("Updating summary notification")
|
||||||
notificationDisplayer.showNotificationMessage(
|
notificationDisplayer.showNotificationMessage(
|
||||||
tag = null,
|
tag = null,
|
||||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||||
notification = summaryNotification.notification
|
notification = summaryNotification.notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +128,8 @@ private fun List<NotifiableEvent>.groupByType(): GroupedNotificationEvents {
|
||||||
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
|
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
|
||||||
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
|
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
|
||||||
is FallbackNotifiableEvent -> fallbackEvents.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)
|
return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
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.R
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
|
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,15 @@ package io.element.android.libraries.push.impl.notifications.channels
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.RingtoneManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.ChecksSdkIntAtLeast
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
|
@ -30,15 +35,51 @@ import io.element.android.libraries.push.impl.R
|
||||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||||
import javax.inject.Inject
|
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.
|
* 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)
|
@SingleIn(AppScope::class)
|
||||||
class NotificationChannels @Inject constructor(
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultNotificationChannels @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val notificationManager: NotificationManagerCompat,
|
private val notificationManager: NotificationManagerCompat,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
) {
|
) : NotificationChannels {
|
||||||
init {
|
init {
|
||||||
createNotificationChannels()
|
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
|
* Default notification importance: shows everywhere, makes noise, but does not visually
|
||||||
* intrude.
|
* intrude.
|
||||||
|
|
@ -123,46 +167,52 @@ class NotificationChannels @Inject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Register a channel for incoming and in progress call notifications with no ringing
|
||||||
notificationManager.createNotificationChannel(
|
notificationManager.createNotificationChannel(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CALL_NOTIFICATION_CHANNEL_ID,
|
CALL_NOTIFICATION_CHANNEL_ID_V3,
|
||||||
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
|
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
)
|
)
|
||||||
.apply {
|
.apply {
|
||||||
description = stringProvider.getString(R.string.notification_channel_call)
|
description = stringProvider.getString(R.string.notification_channel_call)
|
||||||
setSound(null, null)
|
enableVibration(true)
|
||||||
enableLights(true)
|
enableLights(true)
|
||||||
lightColor = accentColor
|
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? {
|
override fun getChannelForIncomingCall(ring: Boolean): String {
|
||||||
return notificationManager.getNotificationChannel(channelId)
|
return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? {
|
override fun getChannelIdForMessage(noisy: Boolean): String {
|
||||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
|
||||||
return getChannel(notificationChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChannelIdForMessage(noisy: Boolean): String {
|
|
||||||
return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
|
override 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,10 @@ import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.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.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.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.RoomEventGroupInfo
|
||||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
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 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) {
|
val builder = if (existingNotification != null) {
|
||||||
NotificationCompat.Builder(context, existingNotification)
|
NotificationCompat.Builder(context, existingNotification)
|
||||||
} else {
|
} else {
|
||||||
NotificationCompat.Builder(context, channelId)
|
NotificationCompat.Builder(context, channelId)
|
||||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
|
||||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
// 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
|
// 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)
|
// 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)
|
setLargeIcon(largeIcon)
|
||||||
}
|
}
|
||||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
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)
|
.setTicker(tickerText)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -343,7 +353,6 @@ class DefaultNotificationCreator @Inject constructor(
|
||||||
.setWhen(lastMessageTimestamp)
|
.setWhen(lastMessageTimestamp)
|
||||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
.setSmallIcon(smallIcon)
|
.setSmallIcon(smallIcon)
|
||||||
// set content text to support devices running API level < 24
|
|
||||||
.setGroup(currentUser.userId.value)
|
.setGroup(currentUser.userId.value)
|
||||||
// set this notification as the summary for the group
|
// set this notification as the summary for the group
|
||||||
.setGroupSummary(true)
|
.setGroupSummary(true)
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ data class NotifiableMessageEvent(
|
||||||
val outGoingMessage: Boolean = false,
|
val outGoingMessage: Boolean = false,
|
||||||
val outGoingMessageFailed: Boolean = false,
|
val outGoingMessageFailed: Boolean = false,
|
||||||
override val isRedacted: Boolean = false,
|
override val isRedacted: Boolean = false,
|
||||||
override val isUpdated: Boolean = false
|
override val isUpdated: Boolean = false,
|
||||||
) : NotifiableEvent {
|
|
||||||
val type: String = EventType.MESSAGE
|
val type: String = EventType.MESSAGE
|
||||||
|
) : NotifiableEvent {
|
||||||
override val description: String = body ?: ""
|
override val description: String = body ?: ""
|
||||||
|
|
||||||
// Example of value:
|
// Example of value:
|
||||||
|
|
@ -69,9 +69,16 @@ fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationSta
|
||||||
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
||||||
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
||||||
null -> false
|
null -> false
|
||||||
else -> appNavigationState.isInForeground &&
|
else -> {
|
||||||
sessionId == currentSessionId &&
|
// Never ignore ringing call notifications
|
||||||
roomId == currentRoomId &&
|
if (this is NotifiableRingingCallEvent) {
|
||||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
false
|
||||||
|
} else {
|
||||||
|
appNavigationState.isInForeground &&
|
||||||
|
sessionId == currentSessionId &&
|
||||||
|
roomId == currentRoomId &&
|
||||||
|
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,11 +17,15 @@
|
||||||
package io.element.android.libraries.push.impl.push
|
package io.element.android.libraries.push.impl.push
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
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.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.test.DefaultTestPush
|
||||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
|
|
@ -44,6 +48,8 @@ class DefaultPushHandler @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||||
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
|
private val notificationChannels: NotificationChannels,
|
||||||
) : PushHandler {
|
) : PushHandler {
|
||||||
/**
|
/**
|
||||||
* Called when message is received.
|
* Called when message is received.
|
||||||
|
|
@ -91,19 +97,33 @@ class DefaultPushHandler @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
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)
|
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||||
if (notifiableEvent == null) {
|
when (notifiableEvent) {
|
||||||
Timber.w("Unable to get a notification data")
|
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||||
return
|
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
|
||||||
|
else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||||
}
|
}
|
||||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
|
||||||
} else {
|
} 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.")
|
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<string name="notification_channel_call">"Call"</string>
|
<string name="notification_channel_call">"Call"</string>
|
||||||
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
||||||
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
||||||
|
<string name="notification_channel_ringing_calls">"Ringing calls"</string>
|
||||||
<string name="notification_channel_silent">"Silent notifications"</string>
|
<string name="notification_channel_silent">"Silent notifications"</string>
|
||||||
<plurals name="notification_compat_summary_line_for_room">
|
<plurals name="notification_compat_summary_line_for_room">
|
||||||
<item quantity="one">"%1$s: %2$d message"</item>
|
<item quantity="one">"%1$s: %2$d message"</item>
|
||||||
|
|
@ -13,6 +14,7 @@
|
||||||
<item quantity="other">"%d notifications"</item>
|
<item quantity="other">"%d notifications"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="notification_fallback_content">"Notification"</string>
|
<string name="notification_fallback_content">"Notification"</string>
|
||||||
|
<string name="notification_incoming_call">"Incoming call"</string>
|
||||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||||
<string name="notification_invitation_action_join">"Join"</string>
|
<string name="notification_invitation_action_join">"Join"</string>
|
||||||
<string name="notification_invitation_action_reject">"Reject"</string>
|
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||||
|
|
|
||||||
|
|
@ -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_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
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.matrix.test.A_SESSION_ID_2
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -33,6 +34,8 @@ import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class DefaultActiveNotificationsProviderTest {
|
class DefaultActiveNotificationsProviderTest {
|
||||||
|
private val notificationIdProvider = NotificationIdProvider
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getAllNotifications with no active notifications returns empty list`() {
|
fun `getAllNotifications with no active notifications returns empty list`() {
|
||||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
|
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
|
||||||
|
|
@ -43,7 +46,6 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getAllNotifications with active notifications returns all`() {
|
fun `getAllNotifications with active notifications returns all`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(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
|
@Test
|
||||||
fun `getNotificationsForSession returns only notifications for that session id`() {
|
fun `getNotificationsForSession returns only notifications for that session id`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
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),
|
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||||
|
|
@ -71,7 +72,6 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
|
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,),
|
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),
|
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
|
||||||
|
|
@ -89,7 +89,6 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() {
|
fun `getMessageNotificationsForRoom returns only message notifications for those session and room ids`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(
|
aStatusBarNotification(
|
||||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||||
|
|
@ -117,7 +116,6 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() {
|
fun `getMembershipNotificationsForRoom returns only membership notifications for those session and room ids`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(
|
aStatusBarNotification(
|
||||||
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID),
|
||||||
|
|
@ -145,7 +143,6 @@ class DefaultActiveNotificationsProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getSummaryNotification returns only the summary notification for that session id if it exists`() {
|
fun `getSummaryNotification returns only the summary notification for that session id if it exists`() {
|
||||||
val notificationIdProvider = NotificationIdProvider()
|
|
||||||
val activeNotifications = listOf(
|
val activeNotifications = listOf(
|
||||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(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(
|
return DefaultActiveNotificationsProvider(
|
||||||
notificationManager = notificationManager,
|
notificationManager = notificationManager,
|
||||||
notificationIdProvider = NotificationIdProvider(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,15 @@ package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.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.NotificationContent
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
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.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
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.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.FileMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
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.FallbackNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
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.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.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.A_FAKE_TIMESTAMP
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -58,6 +63,7 @@ import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.RuntimeEnvironment
|
import org.robolectric.RuntimeEnvironment
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class DefaultNotifiableEventResolverTest {
|
class DefaultNotifiableEventResolverTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -479,6 +485,109 @@ class DefaultNotifiableEventResolverTest {
|
||||||
assertThat(result).isEqualTo(expectedResult)
|
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
|
@Test
|
||||||
fun `resolve null cases`() {
|
fun `resolve null cases`() {
|
||||||
testNull(NotificationContent.MessageLike.CallAnswer)
|
testNull(NotificationContent.MessageLike.CallAnswer)
|
||||||
|
|
@ -558,6 +667,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
content: NotificationContent,
|
content: NotificationContent,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
hasMention: Boolean = false,
|
hasMention: Boolean = false,
|
||||||
|
timestamp: Long = A_TIMESTAMP,
|
||||||
): NotificationData {
|
): NotificationData {
|
||||||
return NotificationData(
|
return NotificationData(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
|
|
@ -570,7 +680,7 @@ class DefaultNotifiableEventResolverTest {
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
isEncrypted = false,
|
isEncrypted = false,
|
||||||
isNoisy = false,
|
isNoisy = false,
|
||||||
timestamp = A_TIMESTAMP,
|
timestamp = timestamp,
|
||||||
content = content,
|
content = content,
|
||||||
hasMention = hasMention,
|
hasMention = hasMention,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
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.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.FakeNotificationCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
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.fake.FakeSummaryGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
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.AppNavigationState
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import io.element.android.services.appnavstate.api.NavigationState
|
import io.element.android.services.appnavstate.api.NavigationState
|
||||||
|
|
@ -164,7 +165,7 @@ class DefaultNotificationDrawerManagerTest {
|
||||||
val notificationManager = mockk<NotificationManagerCompat> {
|
val notificationManager = mockk<NotificationManagerCompat> {
|
||||||
every { cancel(any(), any()) } returns Unit
|
every { cancel(any(), any()) } returns Unit
|
||||||
}
|
}
|
||||||
val summaryId = NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)
|
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
|
||||||
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||||
mutableListOf(
|
mutableListOf(
|
||||||
mockk {
|
mockk {
|
||||||
|
|
@ -198,7 +199,6 @@ class DefaultNotificationDrawerManagerTest {
|
||||||
return DefaultNotificationDrawerManager(
|
return DefaultNotificationDrawerManager(
|
||||||
notificationManager = notificationManager,
|
notificationManager = notificationManager,
|
||||||
notificationRenderer = NotificationRenderer(
|
notificationRenderer = NotificationRenderer(
|
||||||
notificationIdProvider = NotificationIdProvider(),
|
|
||||||
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)),
|
notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)),
|
||||||
notificationDataFactory = DefaultNotificationDataFactory(
|
notificationDataFactory = DefaultNotificationDataFactory(
|
||||||
notificationCreator = FakeNotificationCreator(),
|
notificationCreator = FakeNotificationCreator(),
|
||||||
|
|
@ -208,7 +208,6 @@ class DefaultNotificationDrawerManagerTest {
|
||||||
stringProvider = FakeStringProvider(),
|
stringProvider = FakeStringProvider(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
notificationIdProvider = NotificationIdProvider(),
|
|
||||||
appNavigationStateService = appNavigationStateService,
|
appNavigationStateService = appNavigationStateService,
|
||||||
coroutineScope = this,
|
coroutineScope = this,
|
||||||
matrixClientProvider = matrixClientProvider,
|
matrixClientProvider = matrixClientProvider,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
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.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.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.api.sdk.BuildVersionSdkIntProvider
|
||||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||||
|
|
@ -212,7 +212,7 @@ fun createRoomGroupMessageCreator(
|
||||||
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
|
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
|
||||||
): RoomGroupMessageCreator {
|
): RoomGroupMessageCreator {
|
||||||
val context = RuntimeEnvironment.getApplication() as Context
|
val context = RuntimeEnvironment.getApplication() as Context
|
||||||
val bitmapLoader = NotificationBitmapLoader(
|
val bitmapLoader = DefaultNotificationBitmapLoader(
|
||||||
context = RuntimeEnvironment.getApplication(),
|
context = RuntimeEnvironment.getApplication(),
|
||||||
sdkIntProvider = sdkIntProvider,
|
sdkIntProvider = sdkIntProvider,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_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.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.FakeNotificationCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
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.fake.FakeSummaryGroupMessageCreator
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
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.aSimpleNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
|
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 io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -37,6 +37,7 @@ import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
private val MY_AVATAR_URL: String? = null
|
private val MY_AVATAR_URL: String? = null
|
||||||
|
|
||||||
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
|
||||||
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_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)
|
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue