New Android 12+ audio route picker for calls.

This commit is contained in:
Nicholas
2023-04-03 16:40:18 -04:00
committed by Alex Hart
parent 99bd8e82ca
commit a0aeac767d
25 changed files with 700 additions and 341 deletions

View File

@@ -6,6 +6,8 @@ import androidx.annotation.RequiresApi
@RequiresApi(31)
object AudioDeviceMapping {
val orderOfPreference: List<SignalAudioManager.AudioDevice> = listOf(SignalAudioManager.AudioDevice.BLUETOOTH, SignalAudioManager.AudioDevice.WIRED_HEADSET, SignalAudioManager.AudioDevice.EARPIECE, SignalAudioManager.AudioDevice.SPEAKER_PHONE, SignalAudioManager.AudioDevice.NONE)
private val systemDeviceTypeMap: Map<SignalAudioManager.AudioDevice, List<Int>> = mapOf(
SignalAudioManager.AudioDevice.BLUETOOTH to listOf(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_HEARING_AID),
SignalAudioManager.AudioDevice.EARPIECE to listOf(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE),

View File

@@ -77,18 +77,20 @@ sealed class AudioManagerCommand : Parcelable {
}
}
class SetUserDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() {
class SetUserDevice(val recipientId: RecipientId?, val device: Int, val isId: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device)
parcel.writeInt(device)
ParcelUtil.writeBoolean(parcel, isId)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat {
SetUserDevice(
it.readParcelableCompat(RecipientId::class.java),
it.readSerializableCompat(SignalAudioManager.AudioDevice::class.java)!!
recipientId = it.readParcelableCompat(RecipientId::class.java),
device = it.readInt(),
isId = ParcelUtil.readBoolean(it)
)
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.MediaRouter
import android.net.Uri
import androidx.annotation.RequiresApi
import org.signal.core.util.logging.Log
@@ -19,15 +18,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId
class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
private val TAG = "SignalAudioManager31"
private var currentAudioDevice: AudioDevice = AudioDevice.NONE
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var userSelectedAudioDevice: AudioDeviceInfo? = null
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false
private var savedIsMicrophoneMute = false
private var hasWiredHeadset = false
private var autoSwitchToWiredHeadset = true
private var autoSwitchToBluetooth = true
private val deviceCallback = object : AudioDeviceCallback() {
@@ -56,9 +52,10 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
else -> throw AssertionError("Invalid default audio device selection")
}
if (clearUserEarpieceSelection && userSelectedAudioDevice == AudioDevice.EARPIECE) {
val userSelectedDeviceType: AudioDevice = userSelectedAudioDevice?.type?.let { AudioDeviceMapping.fromPlatformType(it) } ?: AudioDevice.NONE
if (clearUserEarpieceSelection && userSelectedDeviceType == AudioDevice.EARPIECE) {
Log.d(TAG, "Clearing user setting of earpiece")
userSelectedAudioDevice = AudioDevice.NONE
userSelectedAudioDevice = null
}
Log.d(TAG, "New default: $defaultAudioDevice userSelected: $userSelectedAudioDevice")
@@ -124,16 +121,15 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
Log.d(TAG, "Stopped")
}
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
val devices: List<AudioDeviceInfo> = androidAudioManager.availableCommunicationDevices
val availableDevices: List<AudioDevice> = devices.map { AudioDeviceMapping.fromPlatformType(it.type) }
val actualDevice = if (device == AudioDevice.EARPIECE && availableDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device
Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice")
if (!availableDevices.contains(actualDevice)) {
Log.w(TAG, "Can not select $actualDevice from available $availableDevices")
override fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean) {
if (!isId) {
throw IllegalArgumentException("Must supply a device address for API 31+.")
}
userSelectedAudioDevice = actualDevice
Log.d(TAG, "Selecting $device")
userSelectedAudioDevice = androidAudioManager.availableCommunicationDevices.find { it.id == device }
updateAudioDeviceState()
}
@@ -141,7 +137,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
androidAudioManager.mode = AudioManager.MODE_RINGTONE
setMicrophoneMute(false)
setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
setDefaultAudioDevice(recipientId = null, newDefaultDevice = AudioDevice.SPEAKER_PHONE, clearUserEarpieceSelection = false)
incomingRinger.start(ringtoneUri, vibrate)
}
@@ -167,88 +163,35 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
private fun updateAudioDeviceState() {
handler.assertHandlerThread()
val communicationDevice: AudioDeviceInfo? = androidAudioManager.communicationDevice
currentAudioDevice = if (communicationDevice == null) {
AudioDevice.NONE
} else {
AudioDeviceMapping.fromPlatformType(communicationDevice.type)
}
val currentAudioDevice: AudioDeviceInfo? = androidAudioManager.communicationDevice
val availableCommunicationDevices: List<AudioDeviceInfo> = androidAudioManager.availableCommunicationDevices
availableCommunicationDevices.forEach { Log.d(TAG, "Detected communication device of type: ${it.type}") }
val hasBluetoothHeadset = isBluetoothHeadsetConnected()
hasWiredHeadset = availableCommunicationDevices.any { AudioDeviceMapping.fromPlatformType(it.type) == AudioDevice.WIRED_HEADSET }
Log.i(
TAG,
"updateAudioDeviceState(): " +
"wired: $hasWiredHeadset " +
"bt: $hasBluetoothHeadset " +
"available: $availableCommunicationDevices " +
"selected: $selectedAudioDevice " +
"userSelected: $userSelectedAudioDevice"
)
val audioDevices: MutableSet<AudioDevice> = mutableSetOf(AudioDevice.SPEAKER_PHONE)
if (hasBluetoothHeadset) {
audioDevices += AudioDevice.BLUETOOTH
}
if (hasWiredHeadset) {
audioDevices += AudioDevice.WIRED_HEADSET
if (userSelectedAudioDevice != null) {
androidAudioManager.communicationDevice = userSelectedAudioDevice
} else {
autoSwitchToWiredHeadset = true
if (androidAudioManager.hasEarpiece(context)) {
audioDevices += AudioDevice.EARPIECE
}
}
if (!hasBluetoothHeadset && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
userSelectedAudioDevice = AudioDevice.NONE
}
if (hasWiredHeadset && autoSwitchToWiredHeadset) {
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET
autoSwitchToWiredHeadset = false
}
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
userSelectedAudioDevice = AudioDevice.NONE
}
if (!autoSwitchToBluetooth && !hasBluetoothHeadset) {
autoSwitchToBluetooth = true
}
if (autoSwitchToBluetooth && hasBluetoothHeadset) {
userSelectedAudioDevice = AudioDevice.BLUETOOTH
autoSwitchToBluetooth = false
}
val deviceToSet: AudioDevice = when {
audioDevices.contains(userSelectedAudioDevice) -> userSelectedAudioDevice
audioDevices.contains(defaultAudioDevice) -> defaultAudioDevice
else -> AudioDevice.SPEAKER_PHONE
}
if (deviceToSet != currentAudioDevice) {
try {
val chosenDevice: AudioDeviceInfo = availableCommunicationDevices.first { AudioDeviceMapping.getEquivalentPlatformTypes(deviceToSet).contains(it.type) }
val result = androidAudioManager.setCommunicationDevice(chosenDevice)
if (result) {
Log.i(TAG, "Set active device to ID ${chosenDevice.id}, type ${chosenDevice.type}")
currentAudioDevice = deviceToSet
eventListener?.onAudioDeviceChanged(currentAudioDevice, availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
} else {
Log.w(TAG, "Setting device $chosenDevice failed.")
val excludedDevices = emptyList<String>() // TODO: pull this from somewhere. Preferences?
val autoSelectableDevices = availableCommunicationDevices.filterNot { excludedDevices.contains(it.address) }
var candidate: AudioDeviceInfo? = null
val searchOrder: List<AudioDevice> = listOf(defaultAudioDevice) + AudioDeviceMapping.orderOfPreference.filterNot { it == defaultAudioDevice }
for (deviceType in searchOrder) {
candidate = autoSelectableDevices.find { AudioDeviceMapping.fromPlatformType(it.type) == deviceType }
if (candidate != null) {
break
}
}
when (candidate) {
null -> {
Log.e(TAG, "Tried to switch audio devices but could not find suitable device in list of types: ${autoSelectableDevices.map { it.type }.joinToString()}")
androidAudioManager.clearCommunicationDevice()
}
currentAudioDevice -> Log.d(TAG, "Request to switch to existing audio device ignored.")
else -> {
Log.d(TAG, "Switching to new device of type ${candidate.type} from ${currentAudioDevice?.type}")
androidAudioManager.communicationDevice = candidate
eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
}
} catch (e: NoSuchElementException) {
androidAudioManager.clearCommunicationDevice()
}
}
}
private fun isBluetoothHeadsetConnected(): Boolean {
val mediaRouter = context.getSystemService(Context.MEDIA_ROUTER_SERVICE) as MediaRouter
val liveAudioRoute = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO)
return liveAudioRoute.deviceType == MediaRouter.RouteInfo.DEVICE_TYPE_BLUETOOTH
}
}

View File

@@ -39,7 +39,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
companion object {
@JvmStatic
fun create(context: Context, eventListener: EventListener?, isGroup: Boolean): SignalAudioManager {
fun create(context: Context, eventListener: EventListener?): SignalAudioManager {
return if (Build.VERSION.SDK_INT >= 31) {
FullSignalAudioManagerApi31(context, eventListener)
} else {
@@ -55,7 +55,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
is AudioManagerCommand.Start -> start()
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.recipientId, command.device, command.clearUserEarpieceSelection)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.recipientId, command.device)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.recipientId, command.device, command.isId)
is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
@@ -78,7 +78,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
protected abstract fun start()
protected abstract fun stop(playDisconnect: Boolean)
protected abstract fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean)
protected abstract fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice)
protected abstract fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean)
protected abstract fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean)
protected abstract fun startOutgoingRinger()
@@ -95,6 +95,28 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
UNINITIALIZED, PREINITIALIZED, RUNNING
}
/**
* This encapsulates the two ways to represent a chosen audio device.
* Use [desiredAudioDeviceLegacy] for API < 31
* Use [desiredAudioDevice31] for API 31+
*/
class ChosenAudioDeviceIdentifier {
var desiredAudioDeviceLegacy: AudioDevice? = null
var desiredAudioDevice31: Int? = null
fun isLegacy(): Boolean {
return desiredAudioDeviceLegacy != null
}
constructor(device: AudioDevice) {
desiredAudioDeviceLegacy = device
}
constructor(device: Int) {
desiredAudioDevice31 = device
}
}
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
@@ -337,8 +359,12 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
updateAudioDeviceState()
}
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
val actualDevice = if (device == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device
override fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean) {
if (isId) {
throw IllegalArgumentException("Passing audio device address $device to legacy audio manager")
}
val mappedDevice = AudioDevice.values()[device]
val actualDevice: AudioDevice = if (mappedDevice == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else mappedDevice
Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice")
if (!audioDevices.contains(actualDevice)) {
@@ -377,7 +403,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
androidAudioManager.mode = AudioManager.MODE_RINGTONE
setMicrophoneMute(false)
setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
setDefaultAudioDevice(recipientId = null, newDefaultDevice = AudioDevice.SPEAKER_PHONE, clearUserEarpieceSelection = false)
incomingRinger.start(ringtoneUri, vibrate)
}