mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
New Android 12+ audio route picker for calls.
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user