Integrate calling with Android Telecom system.

This commit is contained in:
Cody Henthorne
2022-02-23 13:16:25 -05:00
committed by Alex Hart
parent 2ed39e4448
commit d6b6884c69
31 changed files with 920 additions and 332 deletions

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.webrtc.audio
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.ParcelUtil
/**
@@ -74,19 +75,21 @@ sealed class AudioManagerCommand : Parcelable {
}
}
class SetUserDevice(val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() {
class SetUserDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readSerializable() as SignalAudioManager.AudioDevice) }
val CREATOR: Parcelable.Creator<SetUserDevice> = ParcelCheat { SetUserDevice(it.readParcelable(RecipientId::class.java.classLoader), it.readSerializable() as SignalAudioManager.AudioDevice) }
}
}
class SetDefaultDevice(val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
class SetDefaultDevice(val recipientId: RecipientId?, val device: SignalAudioManager.AudioDevice, val clearUserEarpieceSelection: Boolean) : AudioManagerCommand() {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(recipientId, flags)
parcel.writeSerializable(device)
ParcelUtil.writeBoolean(parcel, clearUserEarpieceSelection)
}
@@ -95,6 +98,7 @@ sealed class AudioManagerCommand : Parcelable {
@JvmField
val CREATOR: Parcelable.Creator<SetDefaultDevice> = ParcelCheat { parcel ->
SetDefaultDevice(
recipientId = parcel.readParcelable(RecipientId::class.java.classLoader),
device = parcel.readSerializable() as SignalAudioManager.AudioDevice,
clearUserEarpieceSelection = ParcelUtil.readBoolean(parcel)
)

View File

@@ -69,7 +69,7 @@ public class IncomingRinger {
player = null;
}
} else {
Log.w(TAG, "Not ringing, player: " + (player != null ? "available" : "null") + " mode: " + ringerMode);
Log.w(TAG, "Not ringing, player: " + (player != null ? "available" : "null") + " modeInt: " + ringerMode + " mode: " + (ringerMode == AudioManager.RINGER_MODE_SILENT ? "silent" : "vibrate only"));
}
}

View File

@@ -12,11 +12,95 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import org.whispersystems.libsignal.util.guava.Preconditions
private val TAG = Log.tag(SignalAudioManager::class.java)
sealed class SignalAudioManager(protected val context: Context, protected val eventListener: EventListener?) {
private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
protected val handler = SignalAudioHandler(commandAndControlThread.looper)
protected var state: State = State.UNINITIALIZED
protected val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
protected var selectedAudioDevice: AudioDevice = AudioDevice.NONE
protected val soundPool: SoundPool = androidAudioManager.createSoundPool()
protected val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
protected val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
protected val incomingRinger = IncomingRinger(context)
protected val outgoingRinger = OutgoingRinger(context)
companion object {
@JvmStatic
fun create(context: Context, eventListener: EventListener?, isGroup: Boolean): SignalAudioManager {
return if (AndroidTelecomUtil.telecomSupported && !isGroup) {
TelecomAwareSignalAudioManager(context, eventListener)
} else {
FullSignalAudioManager(context, eventListener)
}
}
}
fun handleCommand(command: AudioManagerCommand) {
handler.post {
when (command) {
is AudioManagerCommand.Initialize -> initialize()
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.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
}
}
}
fun shutdown() {
handler.post {
stop(false)
if (commandAndControlThread != null) {
Log.i(TAG, "Shutting down command and control")
commandAndControlThread.quitSafely()
commandAndControlThread = null
}
}
}
protected abstract fun initialize()
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 startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean)
protected abstract fun startOutgoingRinger()
protected open fun silenceIncomingRinger() {
Log.i(TAG, "silenceIncomingRinger():")
incomingRinger.stop()
}
enum class AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
}
enum class State {
UNINITIALIZED, PREINITIALIZED, RUNNING
}
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
}
}
/**
* Manage all audio and bluetooth routing for calling. Primarily, operates by maintaining a list
* of available devices (wired, speaker, bluetooth, earpiece) and then using a state machine to determine
@@ -31,15 +115,12 @@ private val TAG = Log.tag(SignalAudioManager::class.java)
* bluetooth headset is then disconnected, and reconnected, the audio will again automatically switch to
* the bluetooth headset.
*/
class SignalAudioManager(private val context: Context, private val eventListener: EventListener?) {
private var commandAndControlThread = SignalExecutors.getAndStartHandlerThread("call-audio")
private val handler = SignalAudioHandler(commandAndControlThread.looper)
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
class FullSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
private val signalBluetoothManager = SignalBluetoothManager(context, this, handler)
private var state: State = State.UNINITIALIZED
private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedIsSpeakerPhoneOn = false
@@ -48,37 +129,9 @@ class SignalAudioManager(private val context: Context, private val eventListener
private var autoSwitchToWiredHeadset = true
private var autoSwitchToBluetooth = true
private var defaultAudioDevice: AudioDevice = AudioDevice.EARPIECE
private var selectedAudioDevice: AudioDevice = AudioDevice.NONE
private var userSelectedAudioDevice: AudioDevice = AudioDevice.NONE
private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
private val soundPool: SoundPool = androidAudioManager.createSoundPool()
private val connectedSoundId = soundPool.load(context, R.raw.webrtc_completed, 1)
private val disconnectedSoundId = soundPool.load(context, R.raw.webrtc_disconnected, 1)
private val incomingRinger = IncomingRinger(context)
private val outgoingRinger = OutgoingRinger(context)
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
fun handleCommand(command: AudioManagerCommand) {
handler.post {
when (command) {
is AudioManagerCommand.Initialize -> initialize()
is AudioManagerCommand.Start -> start()
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.device, command.clearUserEarpieceSelection)
is AudioManagerCommand.SetUserDevice -> selectAudioDevice(command.device)
is AudioManagerCommand.StartIncomingRinger -> startIncomingRinger(command.ringtoneUri, command.vibrate)
is AudioManagerCommand.SilenceIncomingRinger -> silenceIncomingRinger()
is AudioManagerCommand.StartOutgoingRinger -> startOutgoingRinger()
}
}
}
private fun initialize() {
override fun initialize() {
Log.i(TAG, "Initializing audio manager state: $state")
if (state == State.UNINITIALIZED) {
@@ -109,7 +162,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
}
}
private fun start() {
override fun start() {
Log.d(TAG, "Starting. state: $state")
if (state == State.RUNNING) {
Log.w(TAG, "Skipping, already active")
@@ -134,7 +187,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
Log.d(TAG, "Started")
}
private fun stop(playDisconnect: Boolean) {
override fun stop(playDisconnect: Boolean) {
Log.d(TAG, "Stopping. state: $state")
incomingRinger.stop()
@@ -162,17 +215,6 @@ class SignalAudioManager(private val context: Context, private val eventListener
Log.d(TAG, "Stopped")
}
fun shutdown() {
handler.post {
stop(false)
if (commandAndControlThread != null) {
Log.i(TAG, "Shutting down command and control")
commandAndControlThread.quitSafely()
commandAndControlThread = null
}
}
}
fun updateAudioDeviceState() {
handler.assertHandlerThread()
@@ -265,7 +307,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
}
}
private fun setDefaultAudioDevice(newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection")
defaultAudioDevice = when (newDefaultDevice) {
AudioDevice.SPEAKER_PHONE -> newDefaultDevice
@@ -288,7 +330,7 @@ class SignalAudioManager(private val context: Context, private val eventListener
updateAudioDeviceState()
}
private fun selectAudioDevice(device: AudioDevice) {
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
val actualDevice = if (device == AudioDevice.EARPIECE && audioDevices.contains(AudioDevice.WIRED_HEADSET)) AudioDevice.WIRED_HEADSET else device
Log.d(TAG, "selectAudioDevice(): device: $device actualDevice: $actualDevice")
@@ -324,21 +366,16 @@ class SignalAudioManager(private val context: Context, private val eventListener
}
}
private fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
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
setMicrophoneMute(false)
setDefaultAudioDevice(AudioDevice.SPEAKER_PHONE, false)
setDefaultAudioDevice(null, AudioDevice.SPEAKER_PHONE, false)
incomingRinger.start(ringtoneUri, vibrate)
}
private fun silenceIncomingRinger() {
Log.i(TAG, "silenceIncomingRinger():")
incomingRinger.stop()
}
private fun startOutgoingRinger() {
override fun startOutgoingRinger() {
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
@@ -361,17 +398,52 @@ class SignalAudioManager(private val context: Context, private val eventListener
handler.post { onWiredHeadsetChange(pluggedIn, hasMic) }
}
}
}
enum class AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
class TelecomAwareSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
if (recipientId != null && AndroidTelecomUtil.getSelectedAudioDevice(recipientId) == AudioDevice.EARPIECE) {
selectAudioDevice(recipientId, newDefaultDevice)
}
}
enum class State {
UNINITIALIZED, PREINITIALIZED, RUNNING
override fun initialize() {
val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
}
interface EventListener {
@JvmSuppressWildcards
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
override fun start() {
incomingRinger.stop()
outgoingRinger.stop()
val focusedGained = androidAudioManager.requestCallAudioFocus()
if (!focusedGained) {
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
}
}
override fun stop(playDisconnect: Boolean) {
incomingRinger.stop()
outgoingRinger.stop()
androidAudioManager.abandonCallAudioFocus()
}
override fun selectAudioDevice(recipientId: RecipientId?, device: AudioDevice) {
if (recipientId != null) {
selectedAudioDevice = device
AndroidTelecomUtil.selectAudioDevice(recipientId, device)
handler.postDelayed({ AndroidTelecomUtil.selectAudioDevice(recipientId, selectedAudioDevice) }, 1000)
}
}
override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
incomingRinger.start(ringtoneUri, vibrate)
}
override fun startOutgoingRinger() {
outgoingRinger.start(OutgoingRinger.Type.RINGING)
}
}

View File

@@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
*/
class SignalBluetoothManager(
private val context: Context,
private val audioManager: SignalAudioManager,
private val audioManager: FullSignalAudioManager,
private val handler: SignalAudioHandler
) {