Fix Headset Switching (Especially Bluetooth) on Android 12+.

fork-5.53.8
Nicholas 2022-09-27 14:16:24 -04:00 zatwierdzone przez Cody Henthorne
rodzic 1b53f09687
commit 92d065050f
3 zmienionych plików z 184 dodań i 0 usunięć

Wyświetl plik

@ -4,18 +4,23 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.jetbrains.annotations.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.List;
public abstract class AudioManagerCompat {
private static final String TAG = Log.tag(AudioManagerCompat.class);
@ -83,6 +88,36 @@ public abstract class AudioManagerCompat {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
}
@RequiresApi(31)
public List<AudioDeviceInfo> getAvailableCommunicationDevices() {
return audioManager.getAvailableCommunicationDevices();
}
@RequiresApi(31)
public AudioDeviceInfo getCommunicationDevice() {
return audioManager.getCommunicationDevice();
}
@RequiresApi(31)
public boolean setCommunicationDevice(@Nullable AudioDeviceInfo device) {
return audioManager.setCommunicationDevice(device);
}
@RequiresApi(31)
public void clearCommunicationDevice() {
audioManager.clearCommunicationDevice();
}
@RequiresApi(23)
public void registerAudioDeviceCallback(@NonNull AudioDeviceCallback deviceCallback, @NonNull Handler handler) {
audioManager.registerAudioDeviceCallback(deviceCallback, handler);
}
@RequiresApi(23)
public void unregisterAudioDeviceCallback(@NonNull AudioDeviceCallback deviceCallback) {
audioManager.unregisterAudioDeviceCallback(deviceCallback);
}
@SuppressLint("WrongConstant")
public boolean isWiredHeadsetOn() {
if (Build.VERSION.SDK_INT < 23) {

Wyświetl plik

@ -0,0 +1,147 @@
package org.thoughtcrime.securesms.webrtc.audio
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.net.Uri
import androidx.annotation.RequiresApi
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* API 31 introduces new audio manager methods to handle audio routing, including to Bluetooth devices.
* This is important because API 31 also introduces new, more restrictive bluetooth permissioning,
* and the previous SignalAudioManager implementation would have required us to ask for (poorly labeled & scary) Bluetooth permissions.
*/
@RequiresApi(31)
class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
private val TAG = Log.tag(FullSignalAudioManagerApi31::class.java)
private var defaultDevice = AudioDevice.NONE
private val deviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
super.onAudioDevicesAdded(addedDevices)
if (state == State.RUNNING) {
// Switch to any new audio devices immediately.
Log.i(TAG, "onAudioDevicesAdded $addedDevices")
val firstNewlyAddedDevice = addedDevices.firstNotNullOf { fromPlatformType(it.type) }
selectAudioDevice(null, firstNewlyAddedDevice)
}
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
super.onAudioDevicesRemoved(removedDevices)
if (state == State.RUNNING) {
val currentDevice = androidAudioManager.communicationDevice
if (currentDevice != null && removedDevices.map { it.address }.contains(currentDevice.address)) {
selectAudioDevice(null, defaultDevice)
}
}
}
}
private val systemDeviceTypeMap: Map<AudioDevice, List<Int>> = mapOf(
AudioDevice.BLUETOOTH to listOf(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLE_HEADSET),
AudioDevice.EARPIECE to listOf(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE),
AudioDevice.SPEAKER_PHONE to listOf(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE),
AudioDevice.WIRED_HEADSET to listOf(AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_USB_HEADSET),
AudioDevice.NONE to emptyList()
)
private fun getEquivalentPlatformTypes(audioDevice: AudioDevice): List<Int> {
return systemDeviceTypeMap[audioDevice]!!
}
private fun fromPlatformType(type: Int): AudioDevice {
for (kind in AudioDevice.values()) {
if (getEquivalentPlatformTypes(kind).contains(type)) return kind
}
return AudioDevice.NONE
}
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
defaultDevice = newDefaultDevice
}
override fun initialize() {
val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
androidAudioManager.registerAudioDeviceCallback(deviceCallback, handler)
state = State.PREINITIALIZED
}
override fun start() {
incomingRinger.stop()
outgoingRinger.stop()
val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
if (androidAudioManager.availableCommunicationDevices.any { getEquivalentPlatformTypes(AudioDevice.BLUETOOTH).contains(it.type) }) {
selectAudioDevice(null, AudioDevice.BLUETOOTH)
} else if (androidAudioManager.availableCommunicationDevices.any { getEquivalentPlatformTypes(AudioDevice.WIRED_HEADSET).contains(it.type) }) {
selectAudioDevice(null, AudioDevice.WIRED_HEADSET)
}
state = State.RUNNING
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
}
override fun stop(playDisconnect: Boolean) {
incomingRinger.stop()
outgoingRinger.stop()
if (playDisconnect && state != State.UNINITIALIZED) {
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f)
}
androidAudioManager.unregisterAudioDeviceCallback(deviceCallback)
androidAudioManager.clearCommunicationDevice()
state = State.UNINITIALIZED
androidAudioManager.abandonCallAudioFocus()
}
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
val devices: List<AudioDeviceInfo> = androidAudioManager.availableCommunicationDevices
try {
val chosenDevice: AudioDeviceInfo = devices.first { getEquivalentPlatformTypes(device).contains(it.type) }
val result = androidAudioManager.setCommunicationDevice(chosenDevice)
if (result) {
Log.i(TAG, "Set active device to ID ${chosenDevice.id}, type ${chosenDevice.type}")
eventListener?.onAudioDeviceChanged(activeDevice = device, devices = devices.map { fromPlatformType(it.type) }.toSet())
} else {
Log.w(TAG, "Setting device $chosenDevice failed.")
}
} catch (e: NoSuchElementException) {
androidAudioManager.clearCommunicationDevice()
}
}
override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
androidAudioManager.mode = AudioManager.MODE_RINGTONE
if (androidAudioManager.isMicrophoneMute) {
androidAudioManager.isMicrophoneMute = false
}
setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
incomingRinger.start(ringtoneUri, vibrate)
}
override fun startOutgoingRinger() {
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
if (androidAudioManager.isMicrophoneMute) {
androidAudioManager.isMicrophoneMute = false
}
outgoingRinger.start(OutgoingRinger.Type.RINGING)
}
}

Wyświetl plik

@ -42,6 +42,8 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
fun create(context: Context, eventListener: EventListener?, isGroup: Boolean): SignalAudioManager {
return if (AndroidTelecomUtil.telecomSupported && !isGroup) {
TelecomAwareSignalAudioManager(context, eventListener)
} else if (Build.VERSION.SDK_INT >= 31) {
FullSignalAudioManagerApi31(context, eventListener)
} else {
FullSignalAudioManager(context, eventListener)
}