mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Route calls through an entry point the native stack can implement
Adds the seam a native MatrixRTC call stack plugs into, with nothing behind it yet, so that the stack itself lands as a self-contained change. `FeatureFlags.NativeCall` is off by default and unfinished. `features/callnative/api` declares `NativeCallEntryPoint`, and `features/callnative/impl` provides a default that logs and does nothing: turning the flag on today gets no call and a line in the log. That is the honest outcome, where falling back to the WebView would make the flag look broken rather than unimplemented. `DefaultElementCallEntryPoint` forks on the flag. The read is asynchronous because the flag is storage-backed, and caching it in an eagerly started StateFlow would race a call placed just after app start onto the default value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,7 @@ dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.serialization.json)
|
||||
implementation(libs.element.call.embedded)
|
||||
implementation(projects.features.callnative.api)
|
||||
api(projects.features.call.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
|
||||
+22
-1
@@ -16,14 +16,24 @@ 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.features.callnative.api.NativeCallEntryPoint
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultElementCallEntryPoint(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val activeCallManager: ActiveCallManager,
|
||||
private val nativeCallEntryPoint: NativeCallEntryPoint,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
|
||||
) : ElementCallEntryPoint {
|
||||
companion object {
|
||||
const val EXTRA_CALL_TYPE = "EXTRA_CALL_TYPE"
|
||||
@@ -31,7 +41,18 @@ class DefaultElementCallEntryPoint(
|
||||
}
|
||||
|
||||
override fun startCall(callData: CallData) {
|
||||
context.startActivity(IntentProvider.createIntent(context, callData))
|
||||
// The flag is backed by storage, so it has to be read asynchronously. Caching it in an
|
||||
// eagerly started StateFlow would race: a call placed just after app start could be routed
|
||||
// on the default value before the real one arrived.
|
||||
appCoroutineScope.launch {
|
||||
val useNativeCall = featureFlagService.isFeatureEnabled(FeatureFlags.NativeCall)
|
||||
Timber.i("startCall: roomId=${callData.roomId}, nativeCall=$useNativeCall")
|
||||
if (useNativeCall) {
|
||||
nativeCallEntryPoint.startCall(callData)
|
||||
} else {
|
||||
context.startActivity(IntentProvider.createIntent(context, callData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleIncomingCall(
|
||||
|
||||
+34
@@ -16,15 +16,21 @@ 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.features.call.utils.FakeNativeCallEntryPoint
|
||||
import io.element.android.features.callnative.api.NativeCallEntryPoint
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
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 io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
@@ -32,10 +38,13 @@ import org.robolectric.Shadows.shadowOf
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class DefaultElementCallEntryPointTest : RobolectricTest() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
|
||||
val entryPoint = createEntryPoint()
|
||||
entryPoint.startCall(CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false))
|
||||
// The flag is read asynchronously before routing.
|
||||
advanceUntilIdle()
|
||||
|
||||
val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java)
|
||||
val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity
|
||||
@@ -43,6 +52,24 @@ class DefaultElementCallEntryPointTest : RobolectricTest() {
|
||||
assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `startCall - routes to the native call stack when the feature flag is on`() = runTest {
|
||||
val startNativeCallLambda = lambdaRecorder<CallData, Unit> {}
|
||||
val entryPoint = createEntryPoint(
|
||||
nativeCallEntryPoint = FakeNativeCallEntryPoint(startNativeCallLambda),
|
||||
isNativeCallEnabled = true,
|
||||
)
|
||||
val callData = CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = true)
|
||||
|
||||
entryPoint.startCall(callData)
|
||||
advanceUntilIdle()
|
||||
|
||||
startNativeCallLambda.assertions().isCalledOnce().with(value(callData))
|
||||
// The WebView Activity must not also be launched.
|
||||
assertThat(shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity).isNull()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() = runTest {
|
||||
@@ -70,8 +97,15 @@ class DefaultElementCallEntryPointTest : RobolectricTest() {
|
||||
|
||||
private fun TestScope.createEntryPoint(
|
||||
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
|
||||
nativeCallEntryPoint: NativeCallEntryPoint = FakeNativeCallEntryPoint(),
|
||||
isNativeCallEnabled: Boolean = false,
|
||||
) = DefaultElementCallEntryPoint(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
activeCallManager = activeCallManager,
|
||||
nativeCallEntryPoint = nativeCallEntryPoint,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.NativeCall.key to isNativeCallEnabled),
|
||||
),
|
||||
appCoroutineScope = this,
|
||||
)
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.call.utils
|
||||
|
||||
import io.element.android.features.call.api.CallData
|
||||
import io.element.android.features.callnative.api.NativeCallEntryPoint
|
||||
|
||||
class FakeNativeCallEntryPoint(
|
||||
private val startCallLambda: (CallData) -> Unit = {},
|
||||
) : NativeCallEntryPoint {
|
||||
override fun startCall(callData: CallData) = startCallLambda(callData)
|
||||
}
|
||||
Reference in New Issue
Block a user