Merge pull request #3685 from element-hq/feature/bma/callImprovment
Call: ensure that the microphone is working when the application is backgrounded.
This commit is contained in:
commit
2c253be3ab
7 changed files with 72 additions and 24 deletions
|
|
@ -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 @@
|
|||
android:name=".services.CallForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall" />
|
||||
android:foregroundServiceType="microphone" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||
|
|
|
|||
|
|
@ -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
|
|||
class CallForegroundService : Service() {
|
||||
companion object {
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, CallForegroundService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
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() {
|
|||
.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
urlState = urlState.value,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isCallActive = isJoinedCall,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
urlState = urlState,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isCallActive = isCallActive,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue