@@ -12,6 +12,8 @@ import android.content.Context
12
12
import android.media.AudioDeviceCallback
13
13
import android.media.AudioDeviceInfo
14
14
import android.media.AudioManager
15
+ import android.media.AudioManager.OnCommunicationDeviceChangedListener
16
+ import android.os.Build
15
17
import android.util.Log
16
18
import android.view.ViewGroup
17
19
import android.webkit.ConsoleMessage
@@ -20,20 +22,26 @@ import android.webkit.WebChromeClient
20
22
import android.webkit.WebView
21
23
import androidx.activity.compose.BackHandler
22
24
import androidx.compose.foundation.layout.Box
25
+ import androidx.compose.foundation.layout.Column
23
26
import androidx.compose.foundation.layout.consumeWindowInsets
24
27
import androidx.compose.foundation.layout.fillMaxSize
28
+ import androidx.compose.foundation.layout.fillMaxWidth
25
29
import androidx.compose.foundation.layout.padding
26
30
import androidx.compose.material3.ExperimentalMaterial3Api
27
31
import androidx.compose.runtime.Composable
32
+ import androidx.compose.runtime.DisposableEffect
28
33
import androidx.compose.runtime.getValue
34
+ import androidx.compose.runtime.mutableStateListOf
29
35
import androidx.compose.runtime.mutableStateOf
30
36
import androidx.compose.runtime.remember
31
37
import androidx.compose.runtime.setValue
32
38
import androidx.compose.ui.Alignment
33
39
import androidx.compose.ui.Modifier
40
+ import androidx.compose.ui.platform.LocalContext
34
41
import androidx.compose.ui.platform.LocalInspectionMode
35
42
import androidx.compose.ui.res.stringResource
36
43
import androidx.compose.ui.tooling.preview.PreviewParameter
44
+ import androidx.compose.ui.unit.dp
37
45
import androidx.compose.ui.viewinterop.AndroidView
38
46
import androidx.core.content.getSystemService
39
47
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -52,11 +60,15 @@ import io.element.android.libraries.designsystem.components.button.BackButton
52
60
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
53
61
import io.element.android.libraries.designsystem.preview.ElementPreview
54
62
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
63
+ import io.element.android.libraries.designsystem.theme.components.Button
64
+ import io.element.android.libraries.designsystem.theme.components.DropdownMenu
65
+ import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
55
66
import io.element.android.libraries.designsystem.theme.components.Scaffold
56
67
import io.element.android.libraries.designsystem.theme.components.Text
57
68
import io.element.android.libraries.designsystem.theme.components.TopAppBar
58
69
import io.element.android.libraries.ui.strings.CommonStrings
59
70
import timber.log.Timber
71
+ import java.util.concurrent.Executors
60
72
61
73
typealias RequestPermissionCallback = (Array <String >) -> Unit
62
74
@@ -158,28 +170,147 @@ private fun CallWebView(
158
170
Text (" WebView - can't be previewed" )
159
171
}
160
172
} else {
161
- var audioDeviceCallback: AudioDeviceCallback ? by remember { mutableStateOf(null ) }
162
- AndroidView (
163
- modifier = modifier,
164
- factory = { context ->
165
- audioDeviceCallback = context.setupAudioConfiguration()
166
- WebView (context).apply {
167
- onWebViewCreate(this )
168
- setup(userAgent, onPermissionsRequest)
169
- }
170
- },
171
- update = { webView ->
172
- if (url is AsyncData .Success && webView.url != url.data) {
173
- webView.loadUrl(url.data)
173
+ Column (modifier = modifier) {
174
+ var audioDeviceCallback: AudioDeviceCallback ? by remember { mutableStateOf(null ) }
175
+
176
+ OutputAudioDeviceSelector ()
177
+
178
+ AndroidView (
179
+ modifier = Modifier .fillMaxWidth(),
180
+ factory = { context ->
181
+ audioDeviceCallback = context.setupAudioConfiguration()
182
+ WebView (context).apply {
183
+ onWebViewCreate(this )
184
+ setup(userAgent, onPermissionsRequest)
185
+ }
186
+ },
187
+ update = { webView ->
188
+ if (url is AsyncData .Success && webView.url != url.data) {
189
+ webView.loadUrl(url.data)
190
+ }
191
+ },
192
+ onRelease = { webView ->
193
+ // Reset audio mode
194
+ webView.context.releaseAudioConfiguration(audioDeviceCallback)
195
+ webView.destroy()
174
196
}
175
- },
176
- onRelease = { webView ->
177
- // Reset audio mode
178
- webView.context.releaseAudioConfiguration(audioDeviceCallback)
179
- webView.destroy()
197
+ )
198
+ }
199
+ }
200
+ }
201
+
202
+ @Composable
203
+ private fun OutputAudioDeviceSelector () {
204
+ val context = LocalContext .current
205
+ val audioManager = remember { context.getSystemService<AudioManager >() }
206
+
207
+ val audioDevices = remember { mutableStateListOf<AudioDeviceInfo >() }
208
+ var expanded by remember { mutableStateOf(false ) }
209
+ var selected by remember(audioDevices) {
210
+ val device = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
211
+ audioManager?.communicationDevice
212
+ } else {
213
+ null
214
+ }
215
+ mutableStateOf(device)
216
+ }
217
+
218
+ DisposableEffect (Unit ) {
219
+ audioDevices.addAll(audioManager?.loadOutputAudioDevices().orEmpty())
220
+
221
+ val onCommunicationDeviceChangedListener = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
222
+ OnCommunicationDeviceChangedListener { selected = audioManager?.communicationDevice }
223
+ .also { audioManager?.addOnCommunicationDeviceChangedListener(Executors .newSingleThreadExecutor(), it) }
224
+ } else {
225
+ null
226
+ }
227
+
228
+ val audioDeviceCallback = object : AudioDeviceCallback () {
229
+ override fun onAudioDevicesAdded (addedDevices : Array <out AudioDeviceInfo >? ) {
230
+ audioDevices.clear()
231
+ audioDevices.addAll(audioManager?.loadOutputAudioDevices().orEmpty())
232
+ }
233
+
234
+ override fun onAudioDevicesRemoved (removedDevices : Array <out AudioDeviceInfo >? ) {
235
+ audioDevices.clear()
236
+ audioDevices.addAll(audioManager?.loadOutputAudioDevices().orEmpty())
237
+ }
238
+ }
239
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null )
240
+
241
+ onDispose {
242
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
243
+
244
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
245
+ onCommunicationDeviceChangedListener?.let { audioManager?.removeOnCommunicationDeviceChangedListener(it) }
246
+ }
247
+ }
248
+ }
249
+
250
+ Box (Modifier .padding(horizontal = 16 .dp)) {
251
+ Button (
252
+ modifier = Modifier .fillMaxWidth(),
253
+ text = " Audio output: ${selected?.description() ? : " -" } " ,
254
+ onClick = {
255
+ expanded = ! expanded
180
256
}
181
257
)
258
+
259
+ DropdownMenu (expanded, onDismissRequest = { expanded = false }) {
260
+ for (device in audioDevices) {
261
+ DropdownMenuItem (text = {
262
+ Text (device.description())
263
+ }, onClick = {
264
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
265
+ audioManager?.setCommunicationDevice(device)
266
+ selected = device
267
+ expanded = false
268
+ }
269
+ })
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ private fun AudioManager.loadOutputAudioDevices (): List <AudioDeviceInfo > {
276
+ val wantedDeviceTypes = listOf (
277
+ // Paired bluetooth device with microphone
278
+ AudioDeviceInfo .TYPE_BLUETOOTH_SCO ,
279
+ // USB devices which can play or record audio
280
+ AudioDeviceInfo .TYPE_USB_HEADSET ,
281
+ AudioDeviceInfo .TYPE_USB_DEVICE ,
282
+ AudioDeviceInfo .TYPE_USB_ACCESSORY ,
283
+ // Wired audio devices
284
+ AudioDeviceInfo .TYPE_WIRED_HEADSET ,
285
+ AudioDeviceInfo .TYPE_WIRED_HEADPHONES ,
286
+ // The built-in speaker of the device
287
+ AudioDeviceInfo .TYPE_BUILTIN_SPEAKER ,
288
+ // The built-in earpiece of the device
289
+ AudioDeviceInfo .TYPE_BUILTIN_EARPIECE ,
290
+ )
291
+ val devices = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
292
+ availableCommunicationDevices
293
+ } else {
294
+ getDevices(AudioManager .GET_DEVICES_OUTPUTS ).toList()
295
+ }
296
+ return devices.filter { device ->
297
+ wantedDeviceTypes.contains(device.type)
298
+ }
299
+ }
300
+
301
+ fun AudioDeviceInfo.description (): String {
302
+ val type = when (type) {
303
+ AudioDeviceInfo .TYPE_BLUETOOTH_SCO -> " Bluetooth SCO"
304
+ AudioDeviceInfo .TYPE_USB_ACCESSORY -> " USB accessory"
305
+ AudioDeviceInfo .TYPE_USB_DEVICE -> " USB device"
306
+ AudioDeviceInfo .TYPE_USB_HEADSET -> " USB headset"
307
+ AudioDeviceInfo .TYPE_WIRED_HEADSET -> " Wired headset"
308
+ AudioDeviceInfo .TYPE_WIRED_HEADPHONES -> " Wired headphones"
309
+ AudioDeviceInfo .TYPE_BUILTIN_SPEAKER -> " Built-in speaker"
310
+ AudioDeviceInfo .TYPE_BUILTIN_EARPIECE -> " Built-in earpiece"
311
+ else -> " Unknown device type: $type "
182
312
}
313
+ return " $productName - $type "
183
314
}
184
315
185
316
private fun Context.setupAudioConfiguration (): AudioDeviceCallback ? {
0 commit comments