Skip to content

EC: Add PoC for native audio output selector #4663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
package io.element.android.features.call.impl.ui

import android.annotation.SuppressLint
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
Expand All @@ -35,17 +31,15 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.getSystemService
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.WebViewAudioManager
import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.androidutils.compat.disableExternalAudioDevice
import io.element.android.libraries.androidutils.compat.enableExternalAudioDevice
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
Expand Down Expand Up @@ -108,6 +102,7 @@
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
)
} else {
var webViewAudioManager by remember { mutableStateOf<WebViewAudioManager?>(null) }
CallWebView(
modifier = Modifier
.padding(padding)
Expand All @@ -120,14 +115,27 @@
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onWebViewCreate = { webView ->
onCreateWebView = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
onUrlLoaded = { url ->
if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode")

Check warning on line 123 in features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt

View check run for this annotation

Codecov / codecov/patch

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt#L123

Added line #L123 was not covered by tests
webViewAudioManager?.onCallStarted()
} else {
Timber.d("Can't start in-call audio mode since the app is already in it.")

Check warning on line 126 in features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt

View check run for this annotation

Codecov / codecov/patch

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt#L126

Added line #L126 was not covered by tests
}
},
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
)
webViewAudioManager = WebViewAudioManager(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
},
onDestroyWebView = {
// Reset audio mode
webViewAudioManager?.onCallStopped()
}
)
when (state.urlState) {
Expand All @@ -150,21 +158,20 @@
url: AsyncData<String>,
userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit,
onWebViewCreate: (WebView) -> Unit,
onCreateWebView: (WebView) -> Unit,
onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
Text("WebView - can't be previewed")
}
} else {
var audioDeviceCallback: AudioDeviceCallback? by remember { mutableStateOf(null) }
AndroidView(
modifier = modifier,
factory = { context ->
audioDeviceCallback = context.setupAudioConfiguration()
WebView(context).apply {
onWebViewCreate(this)
onCreateWebView(this)
setup(userAgent, onPermissionsRequest)
}
},
Expand All @@ -174,41 +181,13 @@
}
},
onRelease = { webView ->
// Reset audio mode
webView.context.releaseAudioConfiguration(audioDeviceCallback)
onDestroyWebView(webView)
webView.destroy()
}
)
}
}

private fun Context.setupAudioConfiguration(): AudioDeviceCallback? {
val audioManager = getSystemService<AudioManager>() ?: return null
// Set 'voice call' mode so volume keys actually control the call volume
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.enableExternalAudioDevice()
return object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
Timber.d("Audio devices added")
audioManager.enableExternalAudioDevice()
}

override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
Timber.d("Audio devices removed")
audioManager.enableExternalAudioDevice()
}
}.also {
audioManager.registerAudioDeviceCallback(it, null)
}
}

private fun Context.releaseAudioConfiguration(audioDeviceCallback: AudioDeviceCallback?) {
val audioManager = getSystemService<AudioManager>() ?: return
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
audioManager.disableExternalAudioDevice()
audioManager.mode = AudioManager.MODE_NORMAL
}

@SuppressLint("SetJavaScriptEnabled")
private fun WebView.setup(
userAgent: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@

applicationContext.bindings<CallBindings>().inject(this)

@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
)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

Check warning on line 84 in features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L84 was not covered by tests

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)

Check warning on line 87 in features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L87 was not covered by tests
} else {
@Suppress("DEPRECATION")
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)

Check warning on line 90 in features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L90 was not covered by tests
}

setCallType(intent)
// If presenter is not created at this point, it means we have no call to display, the Activity is finishing, so return early
Expand Down
Loading
Loading