diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml index 8337a45b69..8049f66294 100644 --- a/features/call/impl/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -21,8 +21,8 @@ - - + + @@ -80,7 +80,7 @@ android:name=".services.CallForegroundService" android:enabled="true" android:exported="false" - android:foregroundServiceType="phoneCall" /> + android:foregroundServiceType="microphone" /> = Build.VERSION_CODES.Q) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE } else { 0 } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index 3a4a31f190..918acc1200 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor( urlState = urlState.value, webViewError = webViewError, userAgent = userAgent, + isCallActive = isJoinedCall, isInWidgetMode = isInWidgetMode, eventSink = { handleEvents(it) }, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index 48a4672e1a..7da23f24ff 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -13,6 +13,7 @@ data class CallScreenState( val urlState: AsyncData, val webViewError: String?, val userAgent: String, + val isCallActive: Boolean, val isInWidgetMode: Boolean, val eventSink: (CallScreenEvents) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt index bb4794749d..f891c4c526 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -24,6 +24,7 @@ internal fun aCallScreenState( urlState: AsyncData = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), webViewError: String? = null, userAgent: String = "", + isCallActive: Boolean = true, isInWidgetMode: Boolean = false, eventSink: (CallScreenEvents) -> Unit = {}, ): CallScreenState { @@ -31,6 +32,7 @@ internal fun aCallScreenState( urlState = urlState, webViewError = webViewError, userAgent = userAgent, + isCallActive = isCallActive, isInWidgetMode = isInWidgetMode, eventSink = eventSink, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index b55c39f41b..a685ef7b9a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberUpdatedState @@ -95,7 +96,6 @@ class ElementCallActivity : pictureInPicturePresenter.setPipView(this) audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - requestAudioFocus() setContent { val pipState = pictureInPicturePresenter.present() @@ -103,6 +103,12 @@ class ElementCallActivity : ElementThemeApp(appPreferencesStore) { val state = presenter.present() eventSink = state.eventSink + LaunchedEffect(state.isCallActive, state.isInWidgetMode) { + // Note when not in WidgetMode, isCallActive will never be true, so consider the call is active + if (state.isCallActive || !state.isInWidgetMode) { + setCallIsActive() + } + } CallScreenView( state = state, pipState = pipState, @@ -115,6 +121,11 @@ class ElementCallActivity : } } + private fun setCallIsActive() { + requestAudioFocus() + CallForegroundService.start(this) + } + @Composable private fun ListenToAndroidEvents(pipState: PictureInPictureState) { val pipEventSink by rememberUpdatedState(pipState.eventSink) @@ -156,18 +167,6 @@ class ElementCallActivity : setCallType(intent) } - override fun onStart() { - super.onStart() - CallForegroundService.stop(this) - } - - override fun onStop() { - super.onStop() - if (!isFinishing && !isChangingConfigurations) { - CallForegroundService.start(this) - } - } - override fun onDestroy() { super.onDestroy() releaseAudioFocus() @@ -231,10 +230,10 @@ class ElementCallActivity : @Suppress("DEPRECATION") private fun requestAudioFocus() { - val audioAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .build() val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(audioAttributes) .build() @@ -247,7 +246,6 @@ class ElementCallActivity : AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, ) - audioFocusChangeListener = listener } } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index f0e5708db3..d33f16b29e 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -73,6 +73,7 @@ class CallScreenPresenterTest { assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.webViewError).isNull() assertThat(initialState.isInWidgetMode).isFalse() + assertThat(initialState.isCallActive).isFalse() analyticsLambda.assertions().isNeverCalled() joinedCallLambda.assertions().isCalledOnce() } @@ -106,6 +107,7 @@ class CallScreenPresenterTest { joinedCallLambda.assertions().isCalledOnce() val initialState = awaitItem() assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java) + assertThat(initialState.isCallActive).isFalse() assertThat(initialState.isInWidgetMode).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetDriver.runCalledCount).isEqualTo(1) @@ -203,6 +205,44 @@ class CallScreenPresenterTest { } } + @Test + fun `present - a received room member message makes the call to be active`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeMatrixWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + screenTracker = FakeScreenTracker {}, + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isCallActive).isFalse() + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + messageInterceptor.givenInterceptedMessage( + """ + { + "action":"send_event", + "api":"fromWidget", + "widgetId":"1", + "requestId":"1", + "data":{ + "type":"org.matrix.msc3401.call.member" + } + } + """.trimIndent() + ) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.isCallActive).isTrue() + } + } + @Test fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { val navigator = FakeCallScreenNavigator()