mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-16 10:14:33 +01:00
Start callee audio in ringrtc after communications mode change for incoming calls.
Co-authored-by: Jim Gustafson <jim@signal.org>
This commit is contained in:
committed by
Greyson Parrelli
co-authored by
Jim Gustafson
parent
ee59bb4080
commit
017f5d62e2
@@ -276,6 +276,10 @@ class ActiveCallManager(
|
||||
callManager.onAudioDeviceChangeFailed()
|
||||
}
|
||||
|
||||
override fun onAudioReadyForAccept() {
|
||||
callManager.onAudioReadyForAccept()
|
||||
}
|
||||
|
||||
override fun onBluetoothPermissionDenied() {
|
||||
callManager.onBluetoothPermissionDenied()
|
||||
}
|
||||
|
||||
+1
-2
@@ -115,8 +115,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleStartIncomingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
|
||||
remotePeer.answering();
|
||||
|
||||
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
|
||||
|
||||
Log.i(tag, "assign activePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode() + " offerType: " + offerType + " telecomSupported: " + AndroidTelecomUtil.getTelecomSupported());
|
||||
|
||||
boolean isRemoteVideoOffer = currentState.getCallSetupState(remotePeer).isRemoteVideoOffer();
|
||||
Recipient recipient = remotePeer.getRecipient();
|
||||
|
||||
+1
-2
@@ -5,7 +5,6 @@ import androidx.annotation.NonNull;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
@@ -34,7 +33,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Log.i(tag, "handleCallConnected(): call_id: " + remotePeer.getCallId());
|
||||
Log.i(tag, "handleCallConnected(): call_id: " + remotePeer.getCallId() + " callState: " + currentState.getCallInfoState().getCallState());
|
||||
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
|
||||
|
||||
+27
-4
@@ -136,7 +136,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
|
||||
Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId());
|
||||
Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId() + " answerWithVideo: " + answerWithVideo);
|
||||
|
||||
OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter();
|
||||
router.setVanitySink(null);
|
||||
@@ -153,6 +153,28 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
.changeCallSetupState(activePeer.getCallId())
|
||||
.acceptWithVideo(answerWithVideo)
|
||||
.accepted(true)
|
||||
.waitForAudio(true)
|
||||
.build();
|
||||
|
||||
webRtcInteractor.prepareAudioForAccept();
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleAudioReadyForAccept(@NonNull WebRtcServiceState currentState) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
|
||||
|
||||
if (activePeer == null || !currentState.getCallSetupState(activePeer.getCallId()).shouldWaitForAudio()) {
|
||||
Log.w(TAG, "handleAudioReadyForAccept(): call is no longer waiting to be accepted, ignoring");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Log.i(TAG, "handleAudioReadyForAccept(): call_id: " + activePeer.getCallId());
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeCallSetupState(activePeer.getCallId())
|
||||
.waitForAudio(false)
|
||||
.build();
|
||||
|
||||
try {
|
||||
@@ -172,7 +194,8 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (currentState.getCallSetupState(activePeer).isAccepted()) {
|
||||
// Reject deny if the call has already been fully accepted by RingRTC
|
||||
if (currentState.getCallSetupState(activePeer).isAccepted() && !currentState.getCallSetupState(activePeer).shouldWaitForAudio()) {
|
||||
Log.w(TAG, "Cannot deny after call has been accepted!");
|
||||
return currentState;
|
||||
}
|
||||
@@ -240,12 +263,12 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
|
||||
Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
Recipient recipient = remotePeer.getRecipient();
|
||||
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient);
|
||||
|
||||
Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId() + " shouldDisturbUser: " + shouldDisturbUserWithCall);
|
||||
|
||||
activePeer.localRinging();
|
||||
|
||||
SignalDatabase.calls().insertOneToOneCall(remotePeer.getCallId().longValue(),
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleStartOutgoingCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer, @NonNull OfferMessage.Type offerType) {
|
||||
Log.i(TAG, "handleStartOutgoingCall():");
|
||||
Log.i(TAG, "handleStartOutgoingCall(): offerType: " + offerType);
|
||||
WebRtcServiceStateBuilder builder = currentState.builder();
|
||||
|
||||
remotePeer.dialing();
|
||||
|
||||
@@ -381,6 +381,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
||||
process((s, p) -> p.handleAudioDeviceChangeFailed(s));
|
||||
}
|
||||
|
||||
public void onAudioReadyForAccept() {
|
||||
process((s, p) -> p.handleAudioReadyForAccept(s));
|
||||
}
|
||||
|
||||
public void onBluetoothPermissionDenied() {
|
||||
process((s, p) -> p.handleBluetoothPermissionDenied(s));
|
||||
}
|
||||
|
||||
@@ -365,6 +365,11 @@ public abstract class WebRtcActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleAudioReadyForAccept(@NonNull WebRtcServiceState currentState) {
|
||||
Log.i(tag, "handleAudioReadyForAccept not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
|
||||
Log.i(tag, "handleReceivedOfferWhileActive not processed");
|
||||
return currentState;
|
||||
|
||||
@@ -154,6 +154,10 @@ public class WebRtcInteractor {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.Stop(playDisconnect));
|
||||
}
|
||||
|
||||
void prepareAudioForAccept() {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.PrepareForAccept());
|
||||
}
|
||||
|
||||
void startAudioCommunication() {
|
||||
ActiveCallManager.sendAudioManagerCommand(context, new AudioManagerCommand.Start());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ data class CallSetupState(
|
||||
var ringerRecipient: Recipient = Recipient.UNKNOWN,
|
||||
@get:JvmName("shouldWaitForTelecomApproval") var waitForTelecom: Boolean = false,
|
||||
@get:JvmName("isTelecomApproved") var telecomApproved: Boolean = false,
|
||||
@get:JvmName("shouldWaitForAudio") var waitForAudio: Boolean = false,
|
||||
var iceServers: MutableList<PeerConnection.IceServer> = mutableListOf(),
|
||||
@get:JvmName("isAlwaysTurnServers") var alwaysTurnServers: Boolean = false
|
||||
) {
|
||||
|
||||
+5
@@ -211,6 +211,11 @@ public class WebRtcServiceStateBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder waitForAudio(boolean waitForAudio) {
|
||||
toBuild.setWaitForAudio(waitForAudio);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
|
||||
toBuild.setSentJoinedMessage(sentJoinedMessage);
|
||||
return this;
|
||||
|
||||
@@ -62,6 +62,13 @@ sealed class AudioManagerCommand : Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
class PrepareForAccept : AudioManagerCommand() {
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<PrepareForAccept> = ParcelCheat { PrepareForAccept() }
|
||||
}
|
||||
}
|
||||
|
||||
class Start : AudioManagerCommand() {
|
||||
companion object {
|
||||
@JvmField
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioRecordingConfiguration;
|
||||
import android.media.SoundPool;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
@@ -198,6 +199,11 @@ public abstract class AudioManagerCompat {
|
||||
audioManager.unregisterAudioRecordingCallback(callback);
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
public @NonNull List<AudioRecordingConfiguration> getActiveRecordingConfigurations() {
|
||||
return audioManager.getActiveRecordingConfigurations();
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
public boolean isWiredHeadsetOn() {
|
||||
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
|
||||
@@ -323,7 +329,7 @@ public abstract class AudioManagerCompat {
|
||||
.setOnAudioFocusChangeListener(onAudioFocusChangeListener)
|
||||
.build();
|
||||
} else {
|
||||
Log.w(TAG, "Trying again to request audio focus");
|
||||
Log.w(TAG, "Trying to request audio focus");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
+120
-67
@@ -24,6 +24,8 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
private var savedIsSpeakerPhoneOn = false
|
||||
private var hasWiredHeadset = false
|
||||
|
||||
private var appliedCommunicationDeviceId: Int? = null
|
||||
|
||||
private val deviceCallback = object : AudioDeviceCallback() {
|
||||
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
||||
@@ -39,7 +41,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
private val communicationDeviceChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device ->
|
||||
if (device != null) {
|
||||
Log.i(TAG, "OnCommunicationDeviceChangedListener: id: ${device.id} type: ${getDeviceTypeName(device.type)}")
|
||||
Log.i(TAG, "OnCommunicationDeviceChangedListener: id: ${device.id} type: ${getDeviceTypeName(device.type)} mode: ${getModeName(requestedMode)} state: $state")
|
||||
if (state == State.RUNNING && userSelectedAudioDevice != null && device.id != userSelectedAudioDevice?.id) {
|
||||
Log.w(TAG, "OnCommunicationDeviceChangedListener: Device changed to ${device.id} but user selected ${userSelectedAudioDevice?.id}. Re-asserting user selection.")
|
||||
logRoutingContext("OnCommunicationDeviceChangedListener", device)
|
||||
@@ -51,28 +53,41 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
}
|
||||
|
||||
private val modeChangedListener = AudioManager.OnModeChangedListener { mode ->
|
||||
Log.i(TAG, "OnModeChangedListener: ${getModeName(mode)}")
|
||||
if (state == State.RUNNING && mode != AudioManager.MODE_IN_COMMUNICATION) {
|
||||
Log.w(TAG, "OnModeChangedListener: Not MODE_IN_COMMUNICATION during a call. state: $state")
|
||||
logRoutingContext("OnModeChangedListener")
|
||||
Log.i(TAG, "OnModeChangedListener: applied ${getModeName(mode)} (requested ${getModeName(requestedMode)}) state: $state")
|
||||
|
||||
appliedMode = mode
|
||||
|
||||
if (mode == AudioManager.MODE_IN_COMMUNICATION) {
|
||||
Log.i(TAG, "OnModeChangedListener: commDevice: ${describeDevice(androidAudioManager.communicationDevice)} micMute: ${androidAudioManager.isMicrophoneMute}")
|
||||
notifyReadyForAccept("mode applied")
|
||||
} else if (state != State.UNINITIALIZED) {
|
||||
if (state == State.RUNNING) {
|
||||
Log.w(TAG, "OnModeChangedListener: Not MODE_IN_COMMUNICATION during a call. state: $state")
|
||||
}
|
||||
logRoutingContext("OnModeChangedListener", mode = mode)
|
||||
}
|
||||
}
|
||||
|
||||
private val audioRecordingCallback = object : AudioManager.AudioRecordingCallback() {
|
||||
override fun onRecordingConfigChanged(configs: List<AudioRecordingConfiguration>) {
|
||||
if (configs.isEmpty()) {
|
||||
Log.i(TAG, "AudioRecordingCallback: no active recordings")
|
||||
Log.i(TAG, "AudioRecordingCallback: no active recordings state: $state mode: ${getModeName(requestedMode)}")
|
||||
} else {
|
||||
for (config in configs) {
|
||||
val deviceName = config.audioDevice?.let { getDeviceTypeName(it.type) } ?: "null"
|
||||
Log.i(TAG, "AudioRecordingCallback: silenced: ${config.isClientSilenced} source: ${getAudioSourceName(config.audioSource)} device: $deviceName")
|
||||
val description = "AudioRecordingCallback: silenced: ${config.isClientSilenced} source: ${getAudioSourceName(config.audioSource)} device: $deviceName state: $state mode: ${getModeName(requestedMode)}"
|
||||
if (config.isClientSilenced) {
|
||||
Log.w(TAG, description)
|
||||
} else {
|
||||
Log.i(TAG, description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
|
||||
Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection")
|
||||
Log.i(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection state: $state mode: ${getModeName(requestedMode)}")
|
||||
defaultAudioDevice = when (newDefaultDevice) {
|
||||
AudioDevice.SPEAKER_PHONE -> newDefaultDevice
|
||||
AudioDevice.EARPIECE -> {
|
||||
@@ -87,31 +102,26 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
val userSelectedDeviceType: AudioDevice = userSelectedAudioDevice?.type?.let { AudioDeviceMapping.fromPlatformType(it) } ?: AudioDevice.NONE
|
||||
if (clearUserEarpieceSelection && userSelectedDeviceType == AudioDevice.EARPIECE) {
|
||||
Log.d(TAG, "Clearing user setting of earpiece")
|
||||
Log.i(TAG, "Clearing user setting of earpiece")
|
||||
userSelectedAudioDevice = null
|
||||
}
|
||||
|
||||
Log.d(TAG, "New default: $defaultAudioDevice userSelected: ${userSelectedAudioDevice?.id} of type ${userSelectedAudioDevice?.type}")
|
||||
Log.i(TAG, "New default: $defaultAudioDevice userSelected: ${userSelectedAudioDevice?.id} of type ${userSelectedAudioDevice?.type}")
|
||||
updateAudioDeviceState()
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
if (state == State.UNINITIALIZED) {
|
||||
savedAudioMode = androidAudioManager.mode
|
||||
requestedMode = savedAudioMode
|
||||
appliedMode = savedAudioMode
|
||||
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
|
||||
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
|
||||
hasWiredHeadset = androidAudioManager.isWiredHeadsetOn
|
||||
|
||||
Log.i(TAG, "initialize: savedMode: ${getModeName(savedAudioMode)} savedSpeaker: $savedIsSpeakerPhoneOn savedMicMute: $savedIsMicrophoneMute wiredHeadset: $hasWiredHeadset")
|
||||
|
||||
val focusGained = androidAudioManager.requestCallAudioFocus()
|
||||
if (!focusGained) {
|
||||
Log.w(TAG, "initialize: audio focus request failed, scheduling retry")
|
||||
handler.postDelayed({
|
||||
val retryGained = androidAudioManager.requestCallAudioFocus()
|
||||
Log.i(TAG, "initialize: audio focus retry result: $retryGained")
|
||||
}, 500)
|
||||
}
|
||||
requestCallAudioFocus("initialize")
|
||||
|
||||
setMicrophoneMute(false)
|
||||
|
||||
@@ -125,36 +135,50 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
state = State.PREINITIALIZED
|
||||
|
||||
Log.d(TAG, "Initialized")
|
||||
Log.i(TAG, "initialize: complete. mode: ${getModeName(requestedMode)}")
|
||||
} else {
|
||||
Log.i(TAG, "initialize: skipping, state: $state")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareForAccept() {
|
||||
Log.i(TAG, "onPrepareForAccept: state: $state previousMode: ${getModeName(requestedMode)} appliedMode: ${getModeName(appliedMode)}")
|
||||
|
||||
incomingRinger.stop()
|
||||
requestCallAudioFocus("onPrepareForAccept")
|
||||
setMicrophoneMute(false)
|
||||
|
||||
// The mode is global but owned per app, so always claim it even when another app holds it,
|
||||
// but only expect a callback when the global mode actually moves.
|
||||
val expectModeChange = requestedMode != AudioManager.MODE_IN_COMMUNICATION || appliedMode != AudioManager.MODE_IN_COMMUNICATION
|
||||
setMode(AudioManager.MODE_IN_COMMUNICATION, "onPrepareForAccept")
|
||||
|
||||
if (!expectModeChange) {
|
||||
notifyReadyForAccept("no mode change expected")
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Log.i(TAG, "start: currentState: $state currentMode: ${getModeName(androidAudioManager.mode)}")
|
||||
Log.i(TAG, "start: currentState: $state previousMode: ${getModeName(requestedMode)} appliedMode: ${getModeName(appliedMode)} prepared: $preparedForAccept")
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
requestCallAudioFocus("start")
|
||||
|
||||
val focusGained = androidAudioManager.requestCallAudioFocus()
|
||||
if (!focusGained) {
|
||||
Log.w(TAG, "start: audio focus request failed, scheduling retry")
|
||||
handler.postDelayed({
|
||||
val retryGained = androidAudioManager.requestCallAudioFocus()
|
||||
Log.i(TAG, "start: audio focus retry result: $retryGained")
|
||||
}, 500)
|
||||
// Only the accept path can skip this, and only because the listener proves the mode is in effect.
|
||||
if (!preparedForAccept || appliedMode != AudioManager.MODE_IN_COMMUNICATION) {
|
||||
setMode(AudioManager.MODE_IN_COMMUNICATION, "start")
|
||||
}
|
||||
|
||||
state = State.RUNNING
|
||||
Log.i(TAG, "start: setting mode to MODE_IN_COMMUNICATION")
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
logActiveRecordingConfigurations("start")
|
||||
|
||||
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
|
||||
soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f)
|
||||
|
||||
Log.d(TAG, "Started")
|
||||
}
|
||||
|
||||
override fun stop(playDisconnect: Boolean) {
|
||||
Log.i(TAG, "stop: playDisconnect: $playDisconnect currentState: $state")
|
||||
Log.i(TAG, "stop: playDisconnect: $playDisconnect currentState: $state currentMode: ${getModeName(requestedMode)}")
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
@@ -179,17 +203,19 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
"Therefore skipping audio device reset."
|
||||
)
|
||||
} else {
|
||||
Log.i(TAG, "stop: restoring mode to ${getModeName(savedAudioMode)}")
|
||||
Log.i(TAG, "stop: restoring mode to ${getModeName(savedAudioMode)} speaker: $savedIsSpeakerPhoneOn micMute: $savedIsMicrophoneMute")
|
||||
androidAudioManager.clearCommunicationDevice()
|
||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
|
||||
setMicrophoneMute(savedIsMicrophoneMute)
|
||||
androidAudioManager.mode = savedAudioMode
|
||||
setMode(savedAudioMode, "stop")
|
||||
}
|
||||
androidAudioManager.abandonCallAudioFocus()
|
||||
Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams")
|
||||
state = State.UNINITIALIZED
|
||||
resetAcceptState()
|
||||
appliedMode = AudioManager.MODE_INVALID
|
||||
appliedCommunicationDeviceId = null
|
||||
|
||||
Log.d(TAG, "Stopped")
|
||||
Log.i(TAG, "stop: complete. mode: ${getModeName(requestedMode)}")
|
||||
}
|
||||
|
||||
override fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean) {
|
||||
@@ -197,10 +223,10 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
throw IllegalArgumentException("Must supply a device address for API 31+.")
|
||||
}
|
||||
|
||||
Log.d(TAG, "Selecting $device")
|
||||
|
||||
userSelectedAudioDevice = androidAudioManager.availableCommunicationDevices.find { it.id == device }
|
||||
|
||||
Log.i(TAG, "selectAudioDevice(): requested: $device resolved: ${describeDevice(userSelectedAudioDevice)} state: $state mode: ${getModeName(requestedMode)}")
|
||||
|
||||
updateAudioDeviceState()
|
||||
}
|
||||
|
||||
@@ -217,6 +243,14 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
val availableCommunicationDevices: List<AudioDeviceInfo> = androidAudioManager.availableCommunicationDevices
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"updateAudioDeviceState(): state: $state mode: ${getModeName(requestedMode)}\n" +
|
||||
" default: $defaultAudioDevice userSelected: ${describeDevice(userSelectedAudioDevice)}\n" +
|
||||
" current: ${describeDevice(currentAudioDevice)}\n" +
|
||||
" available: ${describeDevices(availableCommunicationDevices)}"
|
||||
)
|
||||
|
||||
if (userSelectedAudioDevice != null && availableCommunicationDevices.none { it.id == userSelectedAudioDevice?.id }) {
|
||||
Log.w(TAG, "User selected device ${userSelectedAudioDevice?.id} of type ${userSelectedAudioDevice?.type?.let { getDeviceTypeName(it) }} is no longer available. Clearing user selection.")
|
||||
userSelectedAudioDevice = null
|
||||
@@ -224,7 +258,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
|
||||
var candidate: AudioDeviceInfo? = userSelectedAudioDevice
|
||||
if (candidate != null && candidate.id != 0) {
|
||||
val result = androidAudioManager.setCommunicationDevice(candidate)
|
||||
val result = setCommunicationDeviceIfNeeded(candidate, currentAudioDevice)
|
||||
if (result) {
|
||||
eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
|
||||
} else {
|
||||
@@ -247,24 +281,50 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
null -> {
|
||||
Log.e(TAG, "Tried to switch audio devices but could not find suitable device in list of types: ${availableCommunicationDevices.map { getDeviceTypeName(it.type) }.joinToString()}")
|
||||
androidAudioManager.clearCommunicationDevice()
|
||||
appliedCommunicationDeviceId = null
|
||||
eventListener?.onAudioDeviceChangeFailed()
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Switching to new device of type ${getDeviceTypeName(candidate.type)} from ${currentAudioDevice?.type?.let { getDeviceTypeName(it) }}")
|
||||
val result = androidAudioManager.setCommunicationDevice(candidate)
|
||||
if (result) {
|
||||
Log.w(TAG, "Succeeded in setting ${candidate.id} (type: ${getDeviceTypeName(candidate.type)}) as communication device.")
|
||||
if (setCommunicationDeviceIfNeeded(candidate, currentAudioDevice)) {
|
||||
eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet())
|
||||
} else {
|
||||
Log.w(TAG, "Failed to set ${candidate.id} as communication device.")
|
||||
eventListener?.onAudioDeviceChangeFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun logRoutingContext(event: String, callbackDevice: AudioDeviceInfo? = null) {
|
||||
val mode = androidAudioManager.mode
|
||||
|
||||
private fun setCommunicationDeviceIfNeeded(device: AudioDeviceInfo, currentDevice: AudioDeviceInfo?): Boolean {
|
||||
if (device.id == appliedCommunicationDeviceId && device.id == currentDevice?.id) {
|
||||
Log.i(TAG, "setCommunicationDevice(${describeDevice(device)}) skipped, already routed")
|
||||
return true
|
||||
}
|
||||
|
||||
val result = androidAudioManager.setCommunicationDevice(device)
|
||||
val description = "setCommunicationDevice(${describeDevice(device)}) returned $result in ${getModeName(requestedMode)}, previous: ${describeDevice(currentDevice)}"
|
||||
if (result) {
|
||||
appliedCommunicationDeviceId = device.id
|
||||
Log.i(TAG, description)
|
||||
} else {
|
||||
Log.w(TAG, description)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun logActiveRecordingConfigurations(event: String) {
|
||||
val configs = androidAudioManager.activeRecordingConfigurations
|
||||
if (configs.isEmpty()) {
|
||||
Log.w(TAG, "$event: activeRecordingConfigurations: none mode: ${getModeName(requestedMode)}")
|
||||
return
|
||||
}
|
||||
for (config in configs) {
|
||||
val deviceName = config.audioDevice?.let { getDeviceTypeName(it.type) } ?: "null"
|
||||
Log.i(TAG, "$event: activeRecordingConfiguration: silenced: ${config.isClientSilenced} source: ${getAudioSourceName(config.audioSource)} device: $deviceName mode: ${getModeName(requestedMode)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRoutingContext(event: String, callbackDevice: AudioDeviceInfo? = null, mode: Int = appliedMode) {
|
||||
val currentDevice: AudioDeviceInfo? = androidAudioManager.communicationDevice
|
||||
val availableDevices: List<AudioDeviceInfo> = androidAudioManager.availableCommunicationDevices
|
||||
val selectedStillAvailable = userSelectedAudioDevice?.let { selected ->
|
||||
@@ -273,19 +333,23 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
val probableCause = when {
|
||||
mode != AudioManager.MODE_IN_COMMUNICATION -> "mode_not_in_communication"
|
||||
userSelectedAudioDevice != null && !selectedStillAvailable -> "user_selected_device_disconnected"
|
||||
state != State.RUNNING -> "expected_before_accept"
|
||||
else -> "platform_or_competing_app_reroute"
|
||||
}
|
||||
Log.w(
|
||||
TAG,
|
||||
"$event: probableCause: $probableCause state: $state mode: ${getModeName(mode)} " +
|
||||
"defaultDevice: $defaultAudioDevice callbackDevice: ${describeDevice(callbackDevice)} " +
|
||||
"userSelected: ${describeDevice(userSelectedAudioDevice)} " +
|
||||
"currentDevice: ${describeDevice(currentDevice)} availableDevices: ${describeDevices(availableDevices)}"
|
||||
)
|
||||
}
|
||||
private fun describeDevices(devices: List<AudioDeviceInfo>): String {
|
||||
return devices.joinToString(prefix = "[", postfix = "]") { describeDevice(it) }
|
||||
val description = "$event: probableCause: $probableCause state: $state mode: ${getModeName(mode)}\n" +
|
||||
" defaultDevice: $defaultAudioDevice callbackDevice: ${describeDevice(callbackDevice)}\n" +
|
||||
" userSelected: ${describeDevice(userSelectedAudioDevice)}\n" +
|
||||
" currentDevice: ${describeDevice(currentDevice)}\n" +
|
||||
" availableDevices: ${describeDevices(availableDevices)}"
|
||||
if (state == State.RUNNING) {
|
||||
Log.w(TAG, description)
|
||||
} else {
|
||||
Log.i(TAG, description)
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeDevices(devices: List<AudioDeviceInfo>): String = devices.joinToString(prefix = "[", postfix = "]") { describeDevice(it) }
|
||||
|
||||
private fun describeDevice(device: AudioDeviceInfo?): String {
|
||||
if (device == null) {
|
||||
return "null"
|
||||
@@ -294,17 +358,6 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener
|
||||
return "${device.id}:${getDeviceTypeName(device.type)}:$productName"
|
||||
}
|
||||
|
||||
private fun getModeName(mode: Int): String {
|
||||
return when (mode) {
|
||||
AudioManager.MODE_NORMAL -> "MODE_NORMAL"
|
||||
AudioManager.MODE_RINGTONE -> "MODE_RINGTONE"
|
||||
AudioManager.MODE_IN_CALL -> "MODE_IN_CALL"
|
||||
AudioManager.MODE_IN_COMMUNICATION -> "MODE_IN_COMMUNICATION"
|
||||
AudioManager.MODE_CALL_SCREENING -> "MODE_CALL_SCREENING"
|
||||
else -> "UNKNOWN($mode)"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceTypeName(type: Int): String {
|
||||
return when (type) {
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "BUILTIN_EARPIECE"
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.media.AudioManager
|
||||
import android.media.SoundPool
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -51,7 +52,28 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
protected var savedAudioMode = AudioManager.MODE_INVALID
|
||||
protected var savedIsMicrophoneMute = false
|
||||
|
||||
/** What we last asked for, which may not be in effect yet. Logged in place of the blocking getMode(). */
|
||||
protected var requestedMode = AudioManager.MODE_INVALID
|
||||
|
||||
/** What the platform last reported as in effect. Branch on this rather than [requestedMode]. */
|
||||
protected var appliedMode = AudioManager.MODE_INVALID
|
||||
|
||||
protected var preparedForAccept = false
|
||||
|
||||
private var awaitingAudioForAccept = false
|
||||
private var prepareForAcceptStartedAt = 0L
|
||||
|
||||
private val acceptGateTimeout = Runnable {
|
||||
Log.w(TAG, "Audio not reported ready within ${ACCEPT_GATE_TIMEOUT_MS}ms, accepting anyway")
|
||||
notifyReadyForAccept("timed out")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FOCUS_RETRY_DELAY_MS = 500L
|
||||
|
||||
@VisibleForTesting
|
||||
const val ACCEPT_GATE_TIMEOUT_MS = 1500L
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@JvmStatic
|
||||
fun create(context: Context, eventListener: EventListener?, canUseTelecom: Boolean): SignalAudioManager {
|
||||
@@ -66,9 +88,11 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
}
|
||||
|
||||
fun handleCommand(command: AudioManagerCommand) {
|
||||
handler.post {
|
||||
val posted = handler.post {
|
||||
Log.i(TAG, "handleCommand(): ${command.javaClass.simpleName} state: $state requestedMode: ${getModeName(requestedMode)}")
|
||||
when (command) {
|
||||
is AudioManagerCommand.Initialize -> initialize()
|
||||
is AudioManagerCommand.PrepareForAccept -> prepareForAccept()
|
||||
is AudioManagerCommand.Start -> start()
|
||||
is AudioManagerCommand.Stop -> stop(command.playDisconnect)
|
||||
is AudioManagerCommand.SetDefaultDevice -> setDefaultAudioDevice(command.recipientId, command.device, command.clearUserEarpieceSelection)
|
||||
@@ -79,6 +103,38 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
is AudioManagerCommand.PlayStateChangeUp -> playStateChangeUp()
|
||||
}
|
||||
}
|
||||
|
||||
if (!posted) {
|
||||
Log.w(TAG, "handleCommand(): could not enqueue ${command.javaClass.simpleName}, handler is shut down")
|
||||
if (command is AudioManagerCommand.PrepareForAccept) {
|
||||
eventListener?.onAudioReadyForAccept()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the backstop first, so an implementation that never reports back only delays the accept. */
|
||||
private fun prepareForAccept() {
|
||||
preparedForAccept = true
|
||||
awaitingAudioForAccept = true
|
||||
prepareForAcceptStartedAt = System.currentTimeMillis()
|
||||
handler.postDelayed(acceptGateTimeout, ACCEPT_GATE_TIMEOUT_MS)
|
||||
onPrepareForAccept()
|
||||
}
|
||||
|
||||
protected fun notifyReadyForAccept(reason: String) {
|
||||
if (!awaitingAudioForAccept) {
|
||||
return
|
||||
}
|
||||
awaitingAudioForAccept = false
|
||||
handler.removeCallbacks(acceptGateTimeout)
|
||||
Log.i(TAG, "notifyReadyForAccept(): $reason after ${System.currentTimeMillis() - prepareForAcceptStartedAt}ms")
|
||||
eventListener?.onAudioReadyForAccept()
|
||||
}
|
||||
|
||||
protected fun resetAcceptState() {
|
||||
preparedForAccept = false
|
||||
awaitingAudioForAccept = false
|
||||
handler.removeCallbacks(acceptGateTimeout)
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
@@ -103,20 +159,69 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
protected abstract fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean)
|
||||
protected abstract fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean)
|
||||
|
||||
/** Must end in [notifyReadyForAccept]; otherwise the accept waits for [ACCEPT_GATE_TIMEOUT_MS]. */
|
||||
protected open fun onPrepareForAccept() {
|
||||
Log.i(TAG, "onPrepareForAccept(): state: $state previousMode: ${getModeName(requestedMode)}")
|
||||
|
||||
incomingRinger.stop()
|
||||
requestCallAudioFocus("onPrepareForAccept")
|
||||
setMicrophoneMute(false)
|
||||
setMode(AudioManager.MODE_IN_COMMUNICATION, "onPrepareForAccept")
|
||||
|
||||
// Nothing reports mode changes before API 31, but the setter blocks there, so a read back
|
||||
// confirms the mode is applied.
|
||||
val readBackStart = System.currentTimeMillis()
|
||||
appliedMode = androidAudioManager.mode
|
||||
Log.i(TAG, "onPrepareForAccept(): mode read back as ${getModeName(appliedMode)} after ${System.currentTimeMillis() - readBackStart}ms")
|
||||
|
||||
notifyReadyForAccept("mode applied per read back")
|
||||
}
|
||||
|
||||
protected open fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) {
|
||||
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate")
|
||||
androidAudioManager.mode = AudioManager.MODE_RINGTONE
|
||||
Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate previousMode: ${getModeName(requestedMode)}")
|
||||
setMode(AudioManager.MODE_RINGTONE, "startIncomingRinger")
|
||||
setMicrophoneMute(false)
|
||||
incomingRinger.start(ringtoneUri, vibrate)
|
||||
}
|
||||
|
||||
protected open fun startOutgoingRinger() {
|
||||
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice")
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice previousMode: ${getModeName(requestedMode)}")
|
||||
setMode(AudioManager.MODE_IN_COMMUNICATION, "startOutgoingRinger")
|
||||
setMicrophoneMute(false)
|
||||
outgoingRinger.start(OutgoingRinger.Type.RINGING)
|
||||
}
|
||||
|
||||
protected fun requestCallAudioFocus(caller: String) {
|
||||
val gained = androidAudioManager.requestCallAudioFocus()
|
||||
Log.i(TAG, "$caller: audio focus gained: $gained")
|
||||
|
||||
if (!gained) {
|
||||
Log.w(TAG, "$caller: audio focus request failed, scheduling retry")
|
||||
handler.postDelayed({
|
||||
Log.i(TAG, "$caller: audio focus retry result: ${androidAudioManager.requestCallAudioFocus()}")
|
||||
}, FOCUS_RETRY_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
/** The setter returns long before the platform applies the change. */
|
||||
protected fun setMode(mode: Int, caller: String) {
|
||||
val start = System.currentTimeMillis()
|
||||
requestedMode = mode
|
||||
androidAudioManager.mode = mode
|
||||
Log.i(TAG, "$caller: requested ${getModeName(mode)}, setter returned in ${System.currentTimeMillis() - start}ms")
|
||||
}
|
||||
|
||||
protected fun getModeName(mode: Int): String {
|
||||
return when (mode) {
|
||||
AudioManager.MODE_NORMAL -> "MODE_NORMAL"
|
||||
AudioManager.MODE_RINGTONE -> "MODE_RINGTONE"
|
||||
AudioManager.MODE_IN_CALL -> "MODE_IN_CALL"
|
||||
AudioManager.MODE_IN_COMMUNICATION -> "MODE_IN_COMMUNICATION"
|
||||
AudioManager.MODE_CALL_SCREENING -> "MODE_CALL_SCREENING"
|
||||
else -> "UNKNOWN($mode)"
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun silenceIncomingRinger() {
|
||||
Log.i(TAG, "silenceIncomingRinger():")
|
||||
incomingRinger.stop()
|
||||
@@ -124,6 +229,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
|
||||
protected fun setMicrophoneMute(on: Boolean) {
|
||||
if (androidAudioManager.isMicrophoneMute != on) {
|
||||
Log.i(TAG, "setMicrophoneMute(): changing system microphone mute to $on")
|
||||
androidAudioManager.isMicrophoneMute = on
|
||||
}
|
||||
}
|
||||
@@ -169,6 +275,9 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
|
||||
fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>)
|
||||
fun onAudioDeviceChangeFailed()
|
||||
fun onBluetoothPermissionDenied()
|
||||
|
||||
/** The device is in MODE_IN_COMMUNICATION and the incoming call may now be accepted. */
|
||||
fun onAudioReadyForAccept()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,18 +311,17 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
|
||||
|
||||
override fun initialize() {
|
||||
Log.i(TAG, "Initializing audio manager state: $state")
|
||||
Log.i(TAG, "initialize(): state: $state")
|
||||
|
||||
if (state == State.UNINITIALIZED) {
|
||||
savedAudioMode = androidAudioManager.mode
|
||||
requestedMode = savedAudioMode
|
||||
appliedMode = savedAudioMode
|
||||
savedIsSpeakerPhoneOn = androidAudioManager.isSpeakerphoneOn
|
||||
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
|
||||
hasWiredHeadset = androidAudioManager.isWiredHeadsetOn
|
||||
|
||||
val focusedGained = androidAudioManager.requestCallAudioFocus()
|
||||
if (!focusedGained) {
|
||||
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
|
||||
}
|
||||
requestCallAudioFocus("initialize")
|
||||
|
||||
setMicrophoneMute(false)
|
||||
|
||||
@@ -227,38 +335,30 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
|
||||
|
||||
state = State.PREINITIALIZED
|
||||
|
||||
Log.d(TAG, "Initialized")
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Log.d(TAG, "Starting. state: $state")
|
||||
Log.i(TAG, "start(): state: $state previousMode: ${getModeName(requestedMode)} appliedMode: ${getModeName(appliedMode)} prepared: $preparedForAccept")
|
||||
if (state == State.RUNNING) {
|
||||
Log.w(TAG, "Skipping, already active")
|
||||
Log.w(TAG, "start(): skipping, already active")
|
||||
return
|
||||
}
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
requestCallAudioFocus("start")
|
||||
|
||||
val focusedGained = androidAudioManager.requestCallAudioFocus()
|
||||
if (!focusedGained) {
|
||||
handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500)
|
||||
}
|
||||
setMode(AudioManager.MODE_IN_COMMUNICATION, "start")
|
||||
|
||||
state = State.RUNNING
|
||||
|
||||
androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
|
||||
val volume: Float = androidAudioManager.ringVolumeWithMinimum()
|
||||
soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f)
|
||||
|
||||
Log.d(TAG, "Started")
|
||||
}
|
||||
|
||||
override fun stop(playDisconnect: Boolean) {
|
||||
Log.d(TAG, "Stopping. state: $state")
|
||||
Log.i(TAG, "stop(): playDisconnect: $playDisconnect state: $state currentMode: ${getModeName(requestedMode)}")
|
||||
|
||||
incomingRinger.stop()
|
||||
outgoingRinger.stop()
|
||||
@@ -269,6 +369,8 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
}
|
||||
|
||||
state = State.UNINITIALIZED
|
||||
resetAcceptState()
|
||||
appliedMode = AudioManager.MODE_INVALID
|
||||
|
||||
context.safeUnregisterReceiver(wiredHeadsetReceiver)
|
||||
wiredHeadsetReceiver = null
|
||||
@@ -277,12 +379,11 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
|
||||
|
||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
|
||||
setMicrophoneMute(savedIsMicrophoneMute)
|
||||
androidAudioManager.mode = savedAudioMode
|
||||
setMode(savedAudioMode, "stop")
|
||||
|
||||
androidAudioManager.abandonCallAudioFocus()
|
||||
Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams")
|
||||
|
||||
Log.d(TAG, "Stopped")
|
||||
Log.i(TAG, "stop(): complete")
|
||||
}
|
||||
|
||||
override fun onAudioDeviceUpdated() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.webrtc.audio
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -35,12 +36,21 @@ class TelecomAudioManager(context: Context, eventListener: EventListener?) : Sig
|
||||
Log.i(TAG, "initialize(): state=$state")
|
||||
if (state == State.UNINITIALIZED) {
|
||||
savedAudioMode = androidAudioManager.mode
|
||||
requestedMode = savedAudioMode
|
||||
appliedMode = savedAudioMode
|
||||
savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute
|
||||
setMicrophoneMute(false)
|
||||
state = State.PREINITIALIZED
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareForAccept() {
|
||||
Log.i(TAG, "onPrepareForAccept(): state=$state, let telecom framework manage the mode")
|
||||
incomingRinger.stop()
|
||||
setMicrophoneMute(false)
|
||||
notifyReadyForAccept("telecom manages the mode")
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Log.i(TAG, "start(): state=$state")
|
||||
if (state == State.RUNNING) {
|
||||
@@ -75,6 +85,8 @@ class TelecomAudioManager(context: Context, eventListener: EventListener?) : Sig
|
||||
}
|
||||
|
||||
state = State.UNINITIALIZED
|
||||
resetAcceptState()
|
||||
appliedMode = AudioManager.MODE_INVALID
|
||||
}
|
||||
|
||||
override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) {
|
||||
|
||||
+34
-1
@@ -65,15 +65,48 @@ class IncomingCallActionProcessorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a ringing video call, when I handleAcceptCall, then I expect the call setup state to be accepted`() {
|
||||
fun `Given a ringing video call, when I handleAcceptCall, then I expect the audio to be prepared before accepting`() {
|
||||
val state = incomingRingingCall()
|
||||
|
||||
val result = processor.handleAcceptCall(state, true)
|
||||
|
||||
assertThat(result.getCallSetupState(CALL_ID).isAccepted).isTrue()
|
||||
verify { webRtcInteractor.prepareAudioForAccept() }
|
||||
verify(exactly = 0) { callManager.acceptCall(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call, when the audio is ready, then I expect the call to be accepted`() {
|
||||
val state = processor.handleAcceptCall(incomingRingingCall(), true)
|
||||
|
||||
processor.handleAudioReadyForAccept(state)
|
||||
|
||||
verify { callManager.acceptCall(CALL_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a call denied while the audio was preparing, when the audio is ready, then I expect no accept`() {
|
||||
val accepted = processor.handleAcceptCall(incomingRingingCall(), true)
|
||||
val denied = processor.handleDenyCall(accepted)
|
||||
|
||||
processor.handleAudioReadyForAccept(denied)
|
||||
|
||||
assertThat(denied.callInfoState.activePeer).isNull()
|
||||
verify(exactly = 0) { callManager.acceptCall(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given an accepted call still waiting on audio, when I handleDenyCall, then I expect the call to be rejected`() {
|
||||
val state = processor.handleAcceptCall(incomingRingingCall(), true)
|
||||
val activePeer = state.callInfoState.requireActivePeer()
|
||||
|
||||
val result = processor.handleDenyCall(state)
|
||||
|
||||
verify { webRtcInteractor.rejectIncomingCall(activePeer.id) }
|
||||
verify { callManager.hangup() }
|
||||
assertThat(result.callInfoState.activePeer).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a ringing unaccepted video call, when I enable vanity, then I expect the vanity camera to start`() {
|
||||
val state = incomingRingingCall(accepted = false, cameraEnabled = false)
|
||||
|
||||
+89
@@ -22,11 +22,13 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -121,6 +123,81 @@ class FullSignalAudioManagerApi31Test {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given the mode was never applied for the accept, when I start, then I expect the mode to be asserted`() {
|
||||
val manager = FullSignalAudioManagerApi31(AppDependencies.application, eventListener)
|
||||
|
||||
try {
|
||||
setState(manager, SignalAudioManager.State.PREINITIALIZED)
|
||||
setPreparedForAccept(manager, true)
|
||||
setAppliedMode(manager, AudioManager.MODE_RINGTONE)
|
||||
|
||||
clearMocks(androidAudioManager, answers = false, recordedCalls = true)
|
||||
|
||||
manager.handleCommand(AudioManagerCommand.Start())
|
||||
|
||||
verify(timeout = 2_000) { androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION }
|
||||
} finally {
|
||||
shutdownManager(manager)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given the mode was applied for the accept, when I start, then I expect no redundant mode change`() {
|
||||
val manager = FullSignalAudioManagerApi31(AppDependencies.application, eventListener)
|
||||
|
||||
try {
|
||||
setState(manager, SignalAudioManager.State.PREINITIALIZED)
|
||||
setPreparedForAccept(manager, true)
|
||||
setAppliedMode(manager, AudioManager.MODE_IN_COMMUNICATION)
|
||||
|
||||
clearMocks(androidAudioManager, answers = false, recordedCalls = true)
|
||||
|
||||
manager.handleCommand(AudioManagerCommand.Start())
|
||||
|
||||
// Waits for the last call start() makes, so the mode decision has already been taken.
|
||||
verify(timeout = 2_000) { androidAudioManager.ringVolumeWithMinimum() }
|
||||
verify(exactly = 0) { androidAudioManager.mode = any() }
|
||||
} finally {
|
||||
shutdownManager(manager)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given the mode change is never reported, when the backstop expires, then I expect the accept to proceed`() {
|
||||
val earpiece = createDevice(30, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, "Phone earpiece")
|
||||
|
||||
every { androidAudioManager.communicationDevice } returns earpiece
|
||||
every { androidAudioManager.availableCommunicationDevices } returns listOf(earpiece)
|
||||
every { androidAudioManager.requestCallAudioFocus() } returns true
|
||||
|
||||
val manager = FullSignalAudioManagerApi31(AppDependencies.application, eventListener)
|
||||
|
||||
try {
|
||||
setState(manager, SignalAudioManager.State.PREINITIALIZED)
|
||||
setAppliedMode(manager, AudioManager.MODE_RINGTONE)
|
||||
|
||||
manager.handleCommand(AudioManagerCommand.PrepareForAccept())
|
||||
|
||||
// The mode change is never reported, so only the backstop can release the accept.
|
||||
idleAudioHandler(manager, SignalAudioManager.ACCEPT_GATE_TIMEOUT_MS / 2)
|
||||
verify(exactly = 0) { eventListener.onAudioReadyForAccept() }
|
||||
|
||||
idleAudioHandler(manager, SignalAudioManager.ACCEPT_GATE_TIMEOUT_MS)
|
||||
verify { eventListener.onAudioReadyForAccept() }
|
||||
} finally {
|
||||
shutdownManager(manager)
|
||||
}
|
||||
}
|
||||
|
||||
private fun idleAudioHandler(manager: FullSignalAudioManagerApi31, millis: Long) {
|
||||
val handlerField = SignalAudioManager::class.java.getDeclaredField("handler")
|
||||
handlerField.isAccessible = true
|
||||
val handler = handlerField.get(manager) as SignalAudioHandler
|
||||
|
||||
shadowOf(handler.looper).idleFor(Duration.ofMillis(millis))
|
||||
}
|
||||
|
||||
private fun createDevice(id: Int, type: Int, productName: String): AudioDeviceInfo {
|
||||
return mockk {
|
||||
every { this@mockk.id } returns id
|
||||
@@ -154,6 +231,18 @@ class FullSignalAudioManagerApi31Test {
|
||||
stateField.set(manager, state)
|
||||
}
|
||||
|
||||
private fun setPreparedForAccept(manager: FullSignalAudioManagerApi31, prepared: Boolean) {
|
||||
val field = SignalAudioManager::class.java.getDeclaredField("preparedForAccept")
|
||||
field.isAccessible = true
|
||||
field.set(manager, prepared)
|
||||
}
|
||||
|
||||
private fun setAppliedMode(manager: FullSignalAudioManagerApi31, mode: Int) {
|
||||
val field = SignalAudioManager::class.java.getDeclaredField("appliedMode")
|
||||
field.isAccessible = true
|
||||
field.set(manager, mode)
|
||||
}
|
||||
|
||||
private fun setUserSelectedAudioDevice(manager: FullSignalAudioManagerApi31, device: AudioDeviceInfo?) {
|
||||
val userSelectedField = FullSignalAudioManagerApi31::class.java.getDeclaredField("userSelectedAudioDevice")
|
||||
userSelectedField.isAccessible = true
|
||||
|
||||
Reference in New Issue
Block a user