@@ -21,8 +21,10 @@ import android.webkit.PermissionRequest
21
21
import android.webkit.WebChromeClient
22
22
import android.webkit.WebView
23
23
import androidx.activity.compose.BackHandler
24
+ import androidx.compose.foundation.layout.Arrangement
24
25
import androidx.compose.foundation.layout.Box
25
26
import androidx.compose.foundation.layout.Column
27
+ import androidx.compose.foundation.layout.Row
26
28
import androidx.compose.foundation.layout.consumeWindowInsets
27
29
import androidx.compose.foundation.layout.fillMaxSize
28
30
import androidx.compose.foundation.layout.fillMaxWidth
@@ -63,8 +65,11 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
63
65
import io.element.android.libraries.designsystem.theme.components.Button
64
66
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
65
67
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
68
+ import io.element.android.libraries.designsystem.theme.components.Icon
69
+ import io.element.android.libraries.designsystem.theme.components.IconSource
66
70
import io.element.android.libraries.designsystem.theme.components.Scaffold
67
71
import io.element.android.libraries.designsystem.theme.components.Text
72
+ import io.element.android.libraries.designsystem.theme.components.TextButton
68
73
import io.element.android.libraries.designsystem.theme.components.TopAppBar
69
74
import io.element.android.libraries.ui.strings.CommonStrings
70
75
import timber.log.Timber
@@ -97,12 +102,15 @@ internal fun CallScreenView(
97
102
topBar = {
98
103
if (! pipState.isInPictureInPicture) {
99
104
TopAppBar (
100
- title = { Text (stringResource( R .string.element_call)) },
105
+ title = {},
101
106
navigationIcon = {
102
107
BackButton (
103
108
imageVector = if (pipState.supportPip) CompoundIcons .ArrowLeft () else CompoundIcons .Close (),
104
109
onClick = ::handleBack,
105
110
)
111
+ },
112
+ actions = {
113
+ AudioDeviceSelector ()
106
114
}
107
115
)
108
116
}
@@ -173,8 +181,6 @@ private fun CallWebView(
173
181
Column (modifier = modifier) {
174
182
var audioDeviceCallback: AudioDeviceCallback ? by remember { mutableStateOf(null ) }
175
183
176
- OutputAudioDeviceSelector ()
177
-
178
184
AndroidView (
179
185
modifier = Modifier .fillMaxWidth(),
180
186
factory = { context ->
@@ -200,66 +206,80 @@ private fun CallWebView(
200
206
}
201
207
202
208
@Composable
203
- private fun OutputAudioDeviceSelector () {
209
+ private fun AudioDeviceSelector (
210
+ modifier : Modifier = Modifier ,
211
+ ) {
212
+ // For now don't display the audio device selector in unsupported Android versions
213
+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .S ) {
214
+ return
215
+ }
204
216
val context = LocalContext .current
205
217
val audioManager = remember { context.getSystemService<AudioManager >() }
206
218
207
219
val audioDevices = remember { mutableStateListOf<AudioDeviceInfo >() }
208
220
var expanded by remember { mutableStateOf(false ) }
221
+ val isInEditMode = LocalInspectionMode .current
209
222
var selected by remember(audioDevices) {
210
- val device = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
223
+ val device = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && ! isInEditMode ) {
211
224
audioManager?.communicationDevice
212
225
} else {
213
226
null
214
227
}
215
228
mutableStateOf(device)
216
229
}
217
230
218
- DisposableEffect (Unit ) {
219
- audioDevices.addAll(audioManager?.loadOutputAudioDevices().orEmpty())
231
+ if (! LocalInspectionMode .current) {
232
+ DisposableEffect (Unit ) {
233
+ audioDevices.addAll(audioManager?.loadCommunicationAudioDevices().orEmpty())
220
234
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())
235
+ val onCommunicationDeviceChangedListener = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
236
+ OnCommunicationDeviceChangedListener { selected = audioManager?.communicationDevice }
237
+ .also { audioManager?.addOnCommunicationDeviceChangedListener(Executors .newSingleThreadExecutor(), it) }
238
+ } else {
239
+ null
232
240
}
233
241
234
- override fun onAudioDevicesRemoved (removedDevices : Array <out AudioDeviceInfo >? ) {
235
- audioDevices.clear()
236
- audioDevices.addAll(audioManager?.loadOutputAudioDevices().orEmpty())
242
+ val audioDeviceCallback = object : AudioDeviceCallback () {
243
+ override fun onAudioDevicesAdded (addedDevices : Array <out AudioDeviceInfo >? ) {
244
+ audioDevices.clear()
245
+ audioDevices.addAll(audioManager?.loadCommunicationAudioDevices().orEmpty())
246
+ }
247
+
248
+ override fun onAudioDevicesRemoved (removedDevices : Array <out AudioDeviceInfo >? ) {
249
+ audioDevices.clear()
250
+ audioDevices.addAll(audioManager?.loadCommunicationAudioDevices().orEmpty())
251
+ }
237
252
}
238
- }
239
- audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null )
253
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null )
240
254
241
- onDispose {
242
- audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
255
+ onDispose {
256
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
243
257
244
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
245
- onCommunicationDeviceChangedListener?.let { audioManager?.removeOnCommunicationDeviceChangedListener(it) }
258
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
259
+ onCommunicationDeviceChangedListener?.let { audioManager?.removeOnCommunicationDeviceChangedListener(it) }
260
+ }
246
261
}
247
262
}
248
263
}
249
264
250
- Box (Modifier .padding(horizontal = 16 .dp)) {
251
- Button (
252
- modifier = Modifier .fillMaxWidth(),
253
- text = " Audio output: ${selected?.description() ? : " -" } " ,
265
+ Box (modifier.padding(horizontal = 16 .dp)) {
266
+ TextButton (
267
+ text = " Audio device" ,
254
268
onClick = {
255
269
expanded = ! expanded
256
- }
270
+ },
271
+ leadingIcon = IconSource .Vector (CompoundIcons .ChevronDown ()),
257
272
)
258
273
259
274
DropdownMenu (expanded, onDismissRequest = { expanded = false }) {
260
275
for (device in audioDevices) {
261
276
DropdownMenuItem (text = {
262
- Text (device.description())
277
+ Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
278
+ Text (device.description())
279
+ if (selected == device) {
280
+ Icon (imageVector = CompoundIcons .Check (), contentDescription = null )
281
+ }
282
+ }
263
283
}, onClick = {
264
284
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
265
285
// Workaround for Android 12, otherwise changing the audio device doesn't work
@@ -300,7 +320,7 @@ private fun OutputAudioDeviceSelector() {
300
320
}
301
321
}
302
322
303
- private fun AudioManager.loadOutputAudioDevices (): List <AudioDeviceInfo > {
323
+ private fun AudioManager.loadCommunicationAudioDevices (): List <AudioDeviceInfo > {
304
324
val wantedDeviceTypes = listOf (
305
325
// Paired bluetooth device with microphone
306
326
AudioDeviceInfo .TYPE_BLUETOOTH_SCO ,
@@ -328,7 +348,7 @@ private fun AudioManager.loadOutputAudioDevices(): List<AudioDeviceInfo> {
328
348
329
349
fun AudioDeviceInfo.description (): String {
330
350
val type = when (type) {
331
- AudioDeviceInfo .TYPE_BLUETOOTH_SCO -> " Bluetooth SCO "
351
+ AudioDeviceInfo .TYPE_BLUETOOTH_SCO -> " Bluetooth"
332
352
AudioDeviceInfo .TYPE_USB_ACCESSORY -> " USB accessory"
333
353
AudioDeviceInfo .TYPE_USB_DEVICE -> " USB device"
334
354
AudioDeviceInfo .TYPE_USB_HEADSET -> " USB headset"
@@ -338,9 +358,25 @@ fun AudioDeviceInfo.description(): String {
338
358
AudioDeviceInfo .TYPE_BUILTIN_EARPIECE -> " Built-in earpiece"
339
359
else -> " Unknown device type: $type "
340
360
}
341
- return " $productName - $type "
361
+ return if (isBuiltIn()) {
362
+ type
363
+ } else {
364
+ val name = if (productName.length > 10 ) {
365
+ productName.substring(0 , 10 ) + " …"
366
+ } else {
367
+ productName
368
+ }
369
+ " $name - $type "
370
+ }
342
371
}
343
372
373
+ private fun AudioDeviceInfo.isBuiltIn (): Boolean = when (type) {
374
+ AudioDeviceInfo .TYPE_BUILTIN_SPEAKER ,
375
+ AudioDeviceInfo .TYPE_BUILTIN_EARPIECE ,
376
+ AudioDeviceInfo .TYPE_BUILTIN_MIC ,
377
+ AudioDeviceInfo .TYPE_BUILTIN_SPEAKER_SAFE -> true
378
+ else -> false }
379
+
344
380
private fun Context.setupAudioConfiguration (): AudioDeviceCallback ? {
345
381
val audioManager = getSystemService<AudioManager >() ? : return null
346
382
// Set 'voice call' mode so volume keys actually control the call volume
0 commit comments