Skip to content

Commit c156fd5

Browse files
authored
Element Call: Add audio output selector handled by Android (#4663)
- Add onUrlLoaded callback to WebViewWidgetMessageInterceptor - Add WebViewAudioManager component and use it instead of the AudioManager extension functions - Enable controlling the audio devices in Element Call from the OS instead of automatically detecting them - Simplify the window flags in ElementCallActivity - Work around the issue where the default audio device wasn't using the right audio stream - Add onAudioPlaybackStarted, use it to start the audio-device related logic
1 parent a873e71 commit c156fd5

File tree

6 files changed

+489
-122
lines changed

6 files changed

+489
-122
lines changed

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

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
package io.element.android.features.call.impl.ui
99

1010
import android.annotation.SuppressLint
11-
import android.content.Context
12-
import android.media.AudioDeviceCallback
13-
import android.media.AudioDeviceInfo
14-
import android.media.AudioManager
1511
import android.util.Log
1612
import android.view.ViewGroup
1713
import android.webkit.ConsoleMessage
@@ -28,24 +24,23 @@ import androidx.compose.runtime.Composable
2824
import androidx.compose.runtime.getValue
2925
import androidx.compose.runtime.mutableStateOf
3026
import androidx.compose.runtime.remember
27+
import androidx.compose.runtime.rememberCoroutineScope
3128
import androidx.compose.runtime.setValue
3229
import androidx.compose.ui.Alignment
3330
import androidx.compose.ui.Modifier
3431
import androidx.compose.ui.platform.LocalInspectionMode
3532
import androidx.compose.ui.res.stringResource
3633
import androidx.compose.ui.tooling.preview.PreviewParameter
3734
import androidx.compose.ui.viewinterop.AndroidView
38-
import androidx.core.content.getSystemService
3935
import io.element.android.compound.tokens.generated.CompoundIcons
4036
import io.element.android.features.call.impl.R
4137
import io.element.android.features.call.impl.pip.PictureInPictureEvents
4238
import io.element.android.features.call.impl.pip.PictureInPictureState
4339
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
4440
import io.element.android.features.call.impl.pip.aPictureInPictureState
41+
import io.element.android.features.call.impl.utils.WebViewAudioManager
4542
import io.element.android.features.call.impl.utils.WebViewPipController
4643
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
47-
import io.element.android.libraries.androidutils.compat.disableExternalAudioDevice
48-
import io.element.android.libraries.androidutils.compat.enableExternalAudioDevice
4944
import io.element.android.libraries.architecture.AsyncData
5045
import io.element.android.libraries.designsystem.components.ProgressDialog
5146
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -108,6 +103,8 @@ internal fun CallScreenView(
108103
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
109104
)
110105
} else {
106+
var webViewAudioManager by remember { mutableStateOf<WebViewAudioManager?>(null) }
107+
val coroutineScope = rememberCoroutineScope()
111108
CallWebView(
112109
modifier = Modifier
113110
.padding(padding)
@@ -120,14 +117,27 @@ internal fun CallScreenView(
120117
val callback: RequestPermissionCallback = { request.grant(it) }
121118
requestPermissions(androidPermissions.toTypedArray(), callback)
122119
},
123-
onWebViewCreate = { webView ->
120+
onCreateWebView = { webView ->
124121
val interceptor = WebViewWidgetMessageInterceptor(
125122
webView = webView,
123+
onUrlLoaded = { url ->
124+
if (webViewAudioManager?.isInCallMode?.get() == false) {
125+
Timber.d("URL $url is loaded, starting in-call audio mode")
126+
webViewAudioManager?.onCallStarted()
127+
} else {
128+
Timber.d("Can't start in-call audio mode since the app is already in it.")
129+
}
130+
},
126131
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
127132
)
133+
webViewAudioManager = WebViewAudioManager(webView, coroutineScope)
128134
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
129135
val pipController = WebViewPipController(webView)
130136
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
137+
},
138+
onDestroyWebView = {
139+
// Reset audio mode
140+
webViewAudioManager?.onCallStopped()
131141
}
132142
)
133143
when (state.urlState) {
@@ -152,21 +162,20 @@ private fun CallWebView(
152162
url: AsyncData<String>,
153163
userAgent: String,
154164
onPermissionsRequest: (PermissionRequest) -> Unit,
155-
onWebViewCreate: (WebView) -> Unit,
165+
onCreateWebView: (WebView) -> Unit,
166+
onDestroyWebView: (WebView) -> Unit,
156167
modifier: Modifier = Modifier,
157168
) {
158169
if (LocalInspectionMode.current) {
159170
Box(modifier = modifier, contentAlignment = Alignment.Center) {
160171
Text("WebView - can't be previewed")
161172
}
162173
} else {
163-
var audioDeviceCallback: AudioDeviceCallback? by remember { mutableStateOf(null) }
164174
AndroidView(
165175
modifier = modifier,
166176
factory = { context ->
167-
audioDeviceCallback = context.setupAudioConfiguration()
168177
WebView(context).apply {
169-
onWebViewCreate(this)
178+
onCreateWebView(this)
170179
setup(userAgent, onPermissionsRequest)
171180
}
172181
},
@@ -176,41 +185,13 @@ private fun CallWebView(
176185
}
177186
},
178187
onRelease = { webView ->
179-
// Reset audio mode
180-
webView.context.releaseAudioConfiguration(audioDeviceCallback)
188+
onDestroyWebView(webView)
181189
webView.destroy()
182190
}
183191
)
184192
}
185193
}
186194

187-
private fun Context.setupAudioConfiguration(): AudioDeviceCallback? {
188-
val audioManager = getSystemService<AudioManager>() ?: return null
189-
// Set 'voice call' mode so volume keys actually control the call volume
190-
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
191-
audioManager.enableExternalAudioDevice()
192-
return object : AudioDeviceCallback() {
193-
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
194-
Timber.d("Audio devices added")
195-
audioManager.enableExternalAudioDevice()
196-
}
197-
198-
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
199-
Timber.d("Audio devices removed")
200-
audioManager.enableExternalAudioDevice()
201-
}
202-
}.also {
203-
audioManager.registerAudioDeviceCallback(it, null)
204-
}
205-
}
206-
207-
private fun Context.releaseAudioConfiguration(audioDeviceCallback: AudioDeviceCallback?) {
208-
val audioManager = getSystemService<AudioManager>() ?: return
209-
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
210-
audioManager.disableExternalAudioDevice()
211-
audioManager.mode = AudioManager.MODE_NORMAL
212-
}
213-
214195
@SuppressLint("SetJavaScriptEnabled")
215196
private fun WebView.setup(
216197
userAgent: String,

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,14 @@ class ElementCallActivity :
8181

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

84-
@Suppress("DEPRECATION")
85-
window.addFlags(
86-
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
87-
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
88-
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
89-
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
90-
)
84+
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
85+
86+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
87+
setShowWhenLocked(true)
88+
} else {
89+
@Suppress("DEPRECATION")
90+
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
91+
}
9192

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

0 commit comments

Comments
 (0)