Browse Source

Merge pull request #3685 from element-hq/feature/bma/callImprovment

Call: ensure that the microphone is working when the application is backgrounded.
pull/3671/head
Benoit Marty 2 days ago committed by GitHub
parent
commit
2c253be3ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      features/call/impl/src/main/AndroidManifest.xml
  2. 10
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt
  3. 1
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
  4. 1
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt
  5. 2
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
  6. 28
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
  7. 40
      features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

6
features/call/impl/src/main/AndroidManifest.xml

@ -21,8 +21,8 @@ @@ -21,8 +21,8 @@
<!-- Permissions for call foreground services -->
<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.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application>
@ -80,7 +80,7 @@ @@ -80,7 +80,7 @@
android:name=".services.CallForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="phoneCall" />
android:foregroundServiceType="microphone" />
<receiver
android:name=".receivers.DeclineCallBroadcastReceiver"

10
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/services/CallForegroundService.kt

@ -7,9 +7,11 @@ @@ -7,9 +7,11 @@
package io.element.android.features.call.impl.services
import android.Manifest
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
@ -33,8 +35,12 @@ import timber.log.Timber @@ -33,8 +35,12 @@ import timber.log.Timber
class CallForegroundService : Service() {
companion object {
fun start(context: Context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
val intent = Intent(context, CallForegroundService::class.java)
ContextCompat.startForegroundService(context, intent)
} else {
Timber.w("Microphone permission is not granted, cannot start the call foreground service")
}
}
fun stop(context: Context) {
@ -67,8 +73,8 @@ class CallForegroundService : Service() { @@ -67,8 +73,8 @@ class CallForegroundService : Service() {
.setContentIntent(pendingIntent)
.build()
val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.ONGOING_CALL)
val serviceType = if (Build.VERSION.SDK_INT >= 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
}

1
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt

@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor( @@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor(
urlState = urlState.value,
webViewError = webViewError,
userAgent = userAgent,
isCallActive = isJoinedCall,
isInWidgetMode = isInWidgetMode,
eventSink = { handleEvents(it) },
)

1
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt

@ -13,6 +13,7 @@ data class CallScreenState( @@ -13,6 +13,7 @@ data class CallScreenState(
val urlState: AsyncData<String>,
val webViewError: String?,
val userAgent: String,
val isCallActive: Boolean,
val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit,
)

2
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt

@ -24,6 +24,7 @@ internal fun aCallScreenState( @@ -24,6 +24,7 @@ internal fun aCallScreenState(
urlState: AsyncData<String> = 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( @@ -31,6 +32,7 @@ internal fun aCallScreenState(
urlState = urlState,
webViewError = webViewError,
userAgent = userAgent,
isCallActive = isCallActive,
isInWidgetMode = isInWidgetMode,
eventSink = eventSink,
)

28
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi @@ -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 : @@ -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 : @@ -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 : @@ -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 : @@ -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 : @@ -231,10 +230,10 @@ class ElementCallActivity :
@Suppress("DEPRECATION")
private fun requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.build()
@ -247,7 +246,6 @@ class ElementCallActivity : @@ -247,7 +246,6 @@ class ElementCallActivity :
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
)
audioFocusChangeListener = listener
}
}

40
features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

@ -73,6 +73,7 @@ class CallScreenPresenterTest { @@ -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 { @@ -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 { @@ -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()

Loading…
Cancel
Save