mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Integrate calling with Android Telecom system.
This commit is contained in:
committed by
Alex Hart
parent
2ed39e4448
commit
d6b6884c69
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user