mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-31 01:05:55 +00:00
android: select audio source (#4040)
* android, desktop: select audio source * improvements * fix possible crash * changes
This commit is contained in:
committed by
GitHub
parent
c8c81a840b
commit
412f75219a
@@ -0,0 +1,235 @@
|
||||
package chat.simplex.common.views.call
|
||||
|
||||
import android.content.Context
|
||||
import android.media.*
|
||||
import android.media.AudioManager.OnCommunicationDeviceChangedListener
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.platform.*
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
interface CallAudioDeviceManagerInterface {
|
||||
val devices: State<List<AudioDeviceInfo>>
|
||||
val currentDevice: MutableState<AudioDeviceInfo?>
|
||||
fun start()
|
||||
fun stop()
|
||||
// AudioDeviceInfo.AudioDeviceType
|
||||
fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean)
|
||||
// AudioDeviceInfo.AudioDeviceType
|
||||
fun selectDevice(id: Int)
|
||||
|
||||
companion object {
|
||||
fun new(): CallAudioDeviceManagerInterface =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PostSCallAudioDeviceManager()
|
||||
} else {
|
||||
PreSCallAudioDeviceManager()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
|
||||
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
|
||||
|
||||
private val audioCallback = object: AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
val oldDevices = devices.value
|
||||
devices.value = am.availableCommunicationDevices
|
||||
Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}")
|
||||
|
||||
if (devices.value.size - oldDevices.size > 0) {
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
devices.value = am.availableCommunicationDevices
|
||||
}
|
||||
}
|
||||
|
||||
private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device ->
|
||||
devices.value = am.availableCommunicationDevices
|
||||
currentDevice.value = device
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
am.registerAudioDeviceCallback(audioCallback, null)
|
||||
am.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), listener)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
am.removeOnCommunicationDeviceChangedListener(listener)
|
||||
}
|
||||
|
||||
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) {
|
||||
Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker")
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
val commDevice = am.communicationDevice
|
||||
if (keepAnyNonEarpiece && commDevice != null && commDevice.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
|
||||
// some external device or speaker selected already, no need to change it
|
||||
return
|
||||
}
|
||||
|
||||
val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
val externalDevice = devices.value.lastOrNull { it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
|
||||
// External device already selected
|
||||
if (externalDevice != null && externalDevice.type == am.communicationDevice?.type) {
|
||||
return
|
||||
}
|
||||
if (externalDevice != null) {
|
||||
am.setCommunicationDevice(externalDevice)
|
||||
} else if (am.communicationDevice?.type != preferredSecondaryDevice) {
|
||||
am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let {
|
||||
am.setCommunicationDevice(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectDevice(id: Int) {
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
val device = devices.value.lastOrNull { it.id == id }
|
||||
if (device != null && am.communicationDevice?.id != id ) {
|
||||
am.setCommunicationDevice(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
|
||||
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
|
||||
|
||||
private val audioCallback = object: AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
val wasSize = devices.value.size
|
||||
devices.value += addedDevices.filter { it.hasSupportedType() }
|
||||
val addedCount = devices.value.size - wasSize
|
||||
//if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
//}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
val wasSize = devices.value.size
|
||||
devices.value = devices.value.filterNot { removedDevices.any { rm -> rm.id == it.id } }
|
||||
//val removedCount = wasSize - devices.value.size
|
||||
//if (devices.value.count { it.hasSupportedType() } == 2 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
am.registerAudioDeviceCallback(audioCallback, null)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
}
|
||||
|
||||
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) {
|
||||
Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker")
|
||||
val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
val externalDevice = devices.value.lastOrNull { it.hasSupportedType() && it.isSource && it.isSink && it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
|
||||
if (externalDevice != null) {
|
||||
selectDevice(externalDevice.id)
|
||||
} else {
|
||||
am.stopBluetoothSco()
|
||||
am.isWiredHeadsetOn = false
|
||||
am.isSpeakerphoneOn = preferredSecondaryDevice == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
am.isBluetoothScoOn = false
|
||||
val newCurrentDevice = devices.value.firstOrNull { it.type == preferredSecondaryDevice }
|
||||
adaptToCurrentlyActiveDevice(newCurrentDevice)
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectDevice(id: Int) {
|
||||
val device = devices.value.lastOrNull { it.id == id }
|
||||
val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
if (isExternalDevice) {
|
||||
am.isSpeakerphoneOn = false
|
||||
if (device?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
|
||||
am.isWiredHeadsetOn = true
|
||||
am.stopBluetoothSco()
|
||||
am.isBluetoothScoOn = false
|
||||
} else {
|
||||
am.isWiredHeadsetOn = false
|
||||
am.startBluetoothSco()
|
||||
am.isBluetoothScoOn = true
|
||||
}
|
||||
adaptToCurrentlyActiveDevice(device)
|
||||
} else {
|
||||
am.stopBluetoothSco()
|
||||
am.isWiredHeadsetOn = false
|
||||
am.isSpeakerphoneOn = device?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
am.isBluetoothScoOn = false
|
||||
adaptToCurrentlyActiveDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
private fun adaptToCurrentlyActiveDevice(newCurrentDevice: AudioDeviceInfo?) {
|
||||
currentDevice.value = newCurrentDevice
|
||||
}
|
||||
|
||||
private fun AudioDeviceInfo.hasSupportedType(): Boolean = when (type) {
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> true
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> true
|
||||
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> true
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET -> true
|
||||
AudioDeviceInfo.TYPE_BLE_SPEAKER -> true
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val AudioDeviceInfo.icon: ImageResource
|
||||
get() = when (this.type) {
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.images.ic_volume_down
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.images.ic_volume_up
|
||||
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
||||
AudioDeviceInfo.TYPE_BLE_SPEAKER -> MR.images.ic_bluetooth
|
||||
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.images.ic_headphones
|
||||
|
||||
AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> MR.images.ic_usb
|
||||
else -> MR.images.ic_brand_awareness_filled
|
||||
}
|
||||
|
||||
val AudioDeviceInfo.name: StringResource?
|
||||
get() = when (this.type) {
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.strings.audio_device_speaker
|
||||
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
||||
AudioDeviceInfo.TYPE_BLE_SPEAKER -> null // Use product name instead
|
||||
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.strings.audio_device_wired_headphones
|
||||
|
||||
AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> null // Use product name instead
|
||||
else -> null // Use product name instead
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -39,14 +38,14 @@ import androidx.webkit.WebViewAssetLoader
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.Contact
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import com.google.accompanist.permissions.*
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
@@ -71,7 +70,6 @@ fun activeCallDestroyWebView() = withApi {
|
||||
actual fun ActiveCallView() {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val scope = rememberCoroutineScope()
|
||||
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
|
||||
val proximityLock = remember {
|
||||
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||
@@ -87,41 +85,16 @@ actual fun ActiveCallView() {
|
||||
wasConnected.value = true
|
||||
}
|
||||
}
|
||||
val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() }
|
||||
DisposableEffect(Unit) {
|
||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
var btDeviceCount = 0
|
||||
val audioCallback = object: AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
btDeviceCount += addedCount
|
||||
audioViaBluetooth.value = btDeviceCount > 0
|
||||
if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
btDeviceCount -= removedCount
|
||||
audioViaBluetooth.value = btDeviceCount > 0
|
||||
if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
}
|
||||
am.registerAudioDeviceCallback(audioCallback, null)
|
||||
callAudioDeviceManager.start()
|
||||
onDispose {
|
||||
CallSoundsPlayer.stop()
|
||||
if (wasConnected.value) {
|
||||
CallSoundsPlayer.vibrate()
|
||||
}
|
||||
dropAudioManagerOverrides()
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
callAudioDeviceManager.stop()
|
||||
if (proximityLock?.isHeld == true) {
|
||||
proximityLock.release()
|
||||
}
|
||||
@@ -147,7 +120,7 @@ actual fun ActiveCallView() {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true)
|
||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||
activeCallWaitDeliveryReceipt(scope)
|
||||
}
|
||||
@@ -168,7 +141,7 @@ actual fun ActiveCallView() {
|
||||
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
||||
if (callStatus == WebRTCCallStatus.Connected) {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) }
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true)
|
||||
}
|
||||
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
|
||||
} catch (e: Throwable) {
|
||||
@@ -177,7 +150,7 @@ actual fun ActiveCallView() {
|
||||
is WCallResponse.Connected -> {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
|
||||
scope.launch {
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true)
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
@@ -223,7 +196,7 @@ actual fun ActiveCallView() {
|
||||
else -> false
|
||||
}
|
||||
if (call != null && showOverlay) {
|
||||
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
|
||||
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
@@ -249,19 +222,21 @@ actual fun ActiveCallView() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState<Boolean>) {
|
||||
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) {
|
||||
ActiveCallOverlayLayout(
|
||||
call = call,
|
||||
speakerCanBeEnabled = !audioViaBluetooth.value,
|
||||
devices = remember { callAudioDeviceManager.devices }.value,
|
||||
currentDevice = remember { callAudioDeviceManager.currentDevice },
|
||||
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
|
||||
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
|
||||
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
|
||||
toggleSound = {
|
||||
var call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
call = call.copy(soundSpeaker = !call.soundSpeaker)
|
||||
chatModel.activeCall.value = call
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true)
|
||||
}
|
||||
},
|
||||
flipCamera = { chatModel.callCommand.add(WCallCommand.Camera(call.localCamera.flipped)) }
|
||||
@@ -272,42 +247,18 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
|
||||
fun ActiveCallOverlayDisabled(call: Call) {
|
||||
ActiveCallOverlayLayout(
|
||||
call = call,
|
||||
speakerCanBeEnabled = false,
|
||||
devices = emptyList(),
|
||||
currentDevice = remember { mutableStateOf(null) },
|
||||
enabled = false,
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
selectDevice = {},
|
||||
toggleVideo = {},
|
||||
toggleSound = {},
|
||||
flipCamera = {}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
|
||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
if (btDevice != null) {
|
||||
am.setCommunicationDevice(btDevice)
|
||||
} else if (am.communicationDevice?.type != preferredSecondaryDevice) {
|
||||
am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let {
|
||||
am.setCommunicationDevice(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (audioViaBluetooth.value) {
|
||||
am.isSpeakerphoneOn = false
|
||||
am.startBluetoothSco()
|
||||
} else {
|
||||
am.stopBluetoothSco()
|
||||
am.isSpeakerphoneOn = speaker
|
||||
}
|
||||
am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropAudioManagerOverrides() {
|
||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.mode = AudioManager.MODE_NORMAL
|
||||
@@ -323,10 +274,12 @@ private fun dropAudioManagerOverrides() {
|
||||
@Composable
|
||||
private fun ActiveCallOverlayLayout(
|
||||
call: Call,
|
||||
speakerCanBeEnabled: Boolean,
|
||||
devices: List<AudioDeviceInfo>,
|
||||
currentDevice: State<AudioDeviceInfo?>,
|
||||
enabled: Boolean = true,
|
||||
dismiss: () -> Unit,
|
||||
toggleAudio: () -> Unit,
|
||||
selectDevice: (AudioDeviceInfo) -> Unit,
|
||||
toggleVideo: () -> Unit,
|
||||
toggleSound: () -> Unit,
|
||||
flipCamera: () -> Unit
|
||||
@@ -339,6 +292,32 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
}
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
@Composable
|
||||
fun SelectSoundDevice() {
|
||||
if (devices.size == 2 &&
|
||||
devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } ||
|
||||
currentDevice.value == null ||
|
||||
devices.none { it.id == currentDevice.value?.id }
|
||||
) {
|
||||
ToggleSoundButton(call, enabled, toggleSound)
|
||||
} else {
|
||||
ExposedDropDownSettingWithIcon(
|
||||
devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
|
||||
currentDevice,
|
||||
fontSize = 18.sp,
|
||||
iconSize = 40.dp,
|
||||
listIconSize = 30.dp,
|
||||
iconColor = Color(0xFFFFFFD8),
|
||||
minWidth = 300.dp,
|
||||
onSelected = {
|
||||
if (it != null) {
|
||||
selectDevice(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (media) {
|
||||
CallMediaType.Video -> {
|
||||
VideoCallInfoView(call)
|
||||
@@ -347,7 +326,7 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
ToggleAudioButton(call, enabled, toggleAudio)
|
||||
Spacer(Modifier.size(40.dp))
|
||||
SelectSoundDevice()
|
||||
IconButton(onClick = dismiss, enabled = enabled) {
|
||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
||||
}
|
||||
@@ -385,7 +364,7 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Box(Modifier.padding(end = 32.dp)) {
|
||||
ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound)
|
||||
SelectSoundDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -780,9 +759,11 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
devices = emptyList(),
|
||||
currentDevice = remember { mutableStateOf(null) },
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
selectDevice = {},
|
||||
toggleVideo = {},
|
||||
toggleSound = {},
|
||||
flipCamera = {}
|
||||
@@ -807,9 +788,11 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
devices = emptyList(),
|
||||
currentDevice = remember { mutableStateOf(null) },
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
selectDevice = {},
|
||||
toggleVideo = {},
|
||||
toggleSound = {},
|
||||
flipCamera = {}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSetting(
|
||||
@@ -79,6 +80,60 @@ fun <T> ExposedDropDownSetting(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSettingWithIcon(
|
||||
values: List<Triple<T, ImageResource, String>>,
|
||||
selection: State<T>,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
iconSize: Dp = 40.dp,
|
||||
listIconSize: Dp = 30.dp,
|
||||
iconColor: Color = MenuTextColor,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
minWidth: Dp = 200.dp,
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
val choice = values.first { it.first == selection.value }
|
||||
Icon(painterResource(choice.second), choice.third, Modifier.size(iconSize), tint = iconColor)
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = minWidth),
|
||||
expanded = expanded,
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onSelected(selectionOption.first)
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Icon(painterResource(selectionOption.second), selectionOption.third, Modifier.size(listIconSize))
|
||||
Spacer(Modifier.width(15.dp))
|
||||
Text(
|
||||
selectionOption.third,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MenuTextColor,
|
||||
fontSize = fontSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSettingRow(
|
||||
title: String,
|
||||
|
||||
@@ -836,6 +836,9 @@
|
||||
<string name="permissions_grant_in_settings">Grant in settings</string>
|
||||
<string name="permissions_find_in_settings_and_grant">Find this permission in Android settings and grant it manually.</string>
|
||||
<string name="permissions_open_settings">Open settings</string>
|
||||
<string name="audio_device_earpiece">Earpiece</string>
|
||||
<string name="audio_device_speaker">Speaker</string>
|
||||
<string name="audio_device_wired_headphones">Headphones</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">The next generation of private messaging</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M451-98v-313L257.5-217.5 217-258l222.5-222L217-702.5l40.5-40.5L451-549.5v-313h28.5L691-651 520-480.5l171 171L479.5-98H451Zm57.5-451.5 102-101.5-102-99.5v201Zm0 339.5 102-99.5-102-101.5v201Z"/></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M653-454.5V-512h142.5v57.5H653Zm32.5 290-117-86 32.5-45 117 86-32.5 45Zm-84.5-500-32.5-45 117-86 32.5 45-117 86ZM125-363v-234h157.5l196-196v626l-196-196H125Z"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M327-124.5H182q-22.969 0-40.234-17.266Q124.5-159.031 124.5-182v-298q0-74.09 28-138.795 28-64.705 76-112.705t112.684-76q64.684-28 138.75-28t138.816 28q64.75 28 112.75 76t76 112.705q28 64.705 28 138.795v298q0 22.969-17.266 40.234Q800.969-124.5 778-124.5H633v-290h145V-480q0-124.193-86.937-211.096Q604.125-778 479.884-778t-211.063 86.904Q182-604.193 182-480v65.5h145v290ZM269.5-357H182v175h87.5v-175Zm421 0v175H778v-175h-87.5Zm-421 0H182h87.5Zm421 0H778h-87.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M670-462.5v-163L569.5-525 544-550.5 667.5-674 544-791l25.5-25.5L670-716v-163h16.5l118 121-86 84 86 92.5-118 119H670Zm39.5-74 44-45-44-44v89Zm0-179.5 44-42-44-51.5v93.5Zm81 591Q677-125 558-180.5T338-338Q236-439 180.5-557.75T125-790.692q0-18.808 12.714-31.558Q150.429-835 169.5-835H306q14 0 23.75 9.75t13.75 24.75l26.929 123.641Q372-663.5 369.5-652q-2.5 11.5-10.229 19.226L261-533q26 44 54.688 81.658Q344.375-413.683 379-380q36.5 38 77.25 69.323Q497-279.353 542-255l95.544-98q9.456-10.5 21.357-14.25T682.5-369l117.362 25.438Q815-340 825-327.801q10 12.198 10 27.301v131q0 19.071-12.714 31.786Q809.571-125 790.5-125ZM232-585.5l81-82-23.5-110H183q1.5 41.5 13 88.25t36 103.75Zm364 358q40 19 88.166 31t93.334 14v-107l-102-21.5-79.5 83.5Zm-364-358Zm364 358Z"/></svg>
|
||||
|
After Width: | Height: | Size: 855 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M480.25-84.5q-31.013 0-50.631-19.559Q410-123.619 410-155q0-21.5 10.75-39.25t30.75-28.197V-334H303q-22.969 0-40.234-17.266Q245.5-368.531 245.5-391.5v-109q-20.5-9-31.25-26.359-10.75-17.358-10.75-39.358 0-31.664 19.535-51.224Q242.569-637 273.916-637t50.965 19.559q19.619 19.56 19.619 51.352 0 22.089-10.75 39.339T303-500.5v109h148.5v-338H373l107-145 107 145h-78v338h148V-496h-41.5v-140.5h141V-496h-42v104.5q0 22.969-17.266 40.234Q679.969-334 657-334H509v111.5q19.95 10.152 30.975 28.733Q551-175.187 551-154.912q0 31.531-19.869 50.971Q511.263-84.5 480.25-84.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 662 B |
Reference in New Issue
Block a user