From 4bbddf736cd0674e6126ff00d4aaf63ba424c891 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Wed, 21 Jan 2026 11:57:48 -0800 Subject: [PATCH] Add more AudioManager logging --- .../webrtc/audio/AudioManagerCompat.java | 49 ++++++- .../audio/FullSignalAudioManagerApi31.kt | 133 ++++++++++++++++-- 2 files changed, 167 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java index 87a6e77f84..e815e227d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioManagerCompat.java @@ -34,7 +34,15 @@ public abstract class AudioManagerCompat { @SuppressWarnings("CodeBlock2Expr") protected final AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = focusChange -> { - Log.i(TAG, "onAudioFocusChangeListener: " + focusChange); + String focusName; + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: focusName = "GAIN"; break; + case AudioManager.AUDIOFOCUS_LOSS: focusName = "LOSS"; break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: focusName = "LOSS_TRANSIENT"; break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: focusName = "LOSS_TRANSIENT_CAN_DUCK"; break; + default: focusName = "UNKNOWN(" + focusChange + ")"; break; + } + Log.i(TAG, "onAudioFocusChangeListener: " + focusName); hasFocus = focusChange == AudioManager.AUDIOFOCUS_GAIN; }; @@ -180,6 +188,16 @@ public abstract class AudioManagerCompat { audioManager.unregisterAudioDeviceCallback(deviceCallback); } + @RequiresApi(24) + public void registerAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback callback, @NonNull Handler handler) { + audioManager.registerAudioRecordingCallback(callback, handler); + } + + @RequiresApi(24) + public void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback callback) { + audioManager.unregisterAudioRecordingCallback(callback); + } + @SuppressLint("WrongConstant") public boolean isWiredHeadsetOn() { AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); @@ -220,13 +238,40 @@ public abstract class AudioManagerCompat { abstract public void abandonCallAudioFocus(); public static AudioManagerCompat create(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= 31) { + return new Api31AudioManagerCompat(context); + } else if (Build.VERSION.SDK_INT >= 26) { return new Api26AudioManagerCompat(context); } else { return new Api21AudioManagerCompat(context); } } + @RequiresApi(31) + static class Api31AudioManagerCompat extends Api26AudioManagerCompat { + + private Api31AudioManagerCompat(@NonNull Context context) { + super(context); + } + + public void addOnModeChangedListener(@NonNull java.util.concurrent.Executor executor, @NonNull AudioManager.OnModeChangedListener listener) { + audioManager.addOnModeChangedListener(executor, listener); + } + + public void removeOnModeChangedListener(@NonNull AudioManager.OnModeChangedListener listener) { + audioManager.removeOnModeChangedListener(listener); + } + + public void addOnCommunicationDeviceChangedListener(@NonNull java.util.concurrent.Executor executor, @NonNull AudioManager.OnCommunicationDeviceChangedListener listener) { + audioManager.addOnCommunicationDeviceChangedListener(executor, listener); + } + + public void removeOnCommunicationDeviceChangedListener(@NonNull AudioManager.OnCommunicationDeviceChangedListener listener) { + audioManager.removeOnCommunicationDeviceChangedListener(listener); + } + + } + @RequiresApi(26) private static class Api26AudioManagerCompat extends AudioManagerCompat { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/FullSignalAudioManagerApi31.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/FullSignalAudioManagerApi31.kt index 472b46668d..046ac64fe0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/FullSignalAudioManagerApi31.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/FullSignalAudioManagerApi31.kt @@ -4,6 +4,8 @@ import android.content.Context import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager +import android.media.AudioRecordingConfiguration +import android.media.MediaRecorder import android.net.Uri import androidx.annotation.RequiresApi import org.signal.core.util.logging.Log @@ -38,6 +40,34 @@ 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)}") + } else { + Log.w(TAG, "OnCommunicationDeviceChangedListener: null") + } + } + + 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") + } + } + + private val audioRecordingCallback = object : AudioManager.AudioRecordingCallback() { + override fun onRecordingConfigChanged(configs: List) { + if (configs.isEmpty()) { + Log.i(TAG, "AudioRecordingCallback: no active recordings") + } 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") + } + } + } + } + override fun setDefaultAudioDevice(recipientId: RecipientId?, newDefaultDevice: AudioDevice, clearUserEarpieceSelection: Boolean) { Log.d(TAG, "setDefaultAudioDevice(): currentDefault: $defaultAudioDevice device: $newDefaultDevice clearUser: $clearUserEarpieceSelection") defaultAudioDevice = when (newDefaultDevice) { @@ -69,15 +99,27 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener savedIsMicrophoneMute = androidAudioManager.isMicrophoneMute hasWiredHeadset = androidAudioManager.isWiredHeadsetOn - val focusedGained = androidAudioManager.requestCallAudioFocus() - if (!focusedGained) { - handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500) + 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) } setMicrophoneMute(false) updateAudioDeviceState() + androidAudioManager.registerAudioDeviceCallback(deviceCallback, handler) + androidAudioManager.registerAudioRecordingCallback(audioRecordingCallback, handler) + val api31AudioManager = androidAudioManager as AudioManagerCompat.Api31AudioManagerCompat + api31AudioManager.addOnModeChangedListener(handler::post, modeChangedListener) + api31AudioManager.addOnCommunicationDeviceChangedListener(handler::post, communicationDeviceChangedListener) + state = State.PREINITIALIZED Log.d(TAG, "Initialized") @@ -85,15 +127,22 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener } override fun start() { + Log.i(TAG, "start: currentState: $state currentMode: ${getModeName(androidAudioManager.mode)}") + incomingRinger.stop() outgoingRinger.stop() - val focusedGained = androidAudioManager.requestCallAudioFocus() - if (!focusedGained) { - handler.postDelayed({ androidAudioManager.requestCallAudioFocus() }, 500) + 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) } state = State.RUNNING + Log.i(TAG, "start: setting mode to MODE_IN_COMMUNICATION") androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION val volume: Float = androidAudioManager.ringVolumeWithMinimum() soundPool.play(connectedSoundId, volume, volume, 0, 0, 1.0f) @@ -102,6 +151,8 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener } override fun stop(playDisconnect: Boolean) { + Log.i(TAG, "stop: playDisconnect: $playDisconnect currentState: $state") + incomingRinger.stop() outgoingRinger.stop() @@ -109,7 +160,14 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener val volume: Float = androidAudioManager.ringVolumeWithMinimum() soundPool.play(disconnectedSoundId, volume, volume, 0, 0, 1.0f) } - androidAudioManager.unregisterAudioDeviceCallback(deviceCallback) + if (state != State.UNINITIALIZED) { + androidAudioManager.unregisterAudioDeviceCallback(deviceCallback) + androidAudioManager.unregisterAudioRecordingCallback(audioRecordingCallback) + val api31AudioManager = androidAudioManager as AudioManagerCompat.Api31AudioManagerCompat + api31AudioManager.removeOnModeChangedListener(modeChangedListener) + api31AudioManager.removeOnCommunicationDeviceChangedListener(communicationDeviceChangedListener) + } + if (state == State.UNINITIALIZED && userSelectedAudioDevice != null) { Log.d( TAG, @@ -118,6 +176,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener "Therefore skipping audio device reset." ) } else { + Log.i(TAG, "stop: restoring mode to ${getModeName(savedAudioMode)}") androidAudioManager.clearCommunicationDevice() setSpeakerphoneOn(savedIsSpeakerPhoneOn) setMicrophoneMute(savedIsMicrophoneMute) @@ -143,7 +202,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener } override fun startIncomingRinger(ringtoneUri: Uri?, vibrate: Boolean) { - Log.i(TAG, "startIncomingRinger(): uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate") + Log.i(TAG, "startIncomingRinger: uri: ${if (ringtoneUri != null) "present" else "null"} vibrate: $vibrate currentMode: ${getModeName(androidAudioManager.mode)}") androidAudioManager.mode = AudioManager.MODE_RINGTONE setMicrophoneMute(false) setDefaultAudioDevice(recipientId = null, newDefaultDevice = AudioDevice.SPEAKER_PHONE, clearUserEarpieceSelection = false) @@ -151,7 +210,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener } override fun startOutgoingRinger() { - Log.i(TAG, "startOutgoingRinger(): currentDevice: $selectedAudioDevice") + Log.i(TAG, "startOutgoingRinger: currentDevice: $selectedAudioDevice currentMode: ${getModeName(androidAudioManager.mode)}") androidAudioManager.mode = AudioManager.MODE_IN_COMMUNICATION setMicrophoneMute(false) outgoingRinger.start(OutgoingRinger.Type.RINGING) @@ -181,7 +240,7 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener if (result) { eventListener?.onAudioDeviceChanged(AudioDeviceMapping.fromPlatformType(candidate.type), availableCommunicationDevices.map { AudioDeviceMapping.fromPlatformType(it.type) }.toSet()) } else { - Log.w(TAG, "Failed to set ${candidate.id} of type ${candidate.type}as communication device.") + Log.w(TAG, "Failed to set ${candidate.id} of type ${getDeviceTypeName(candidate.type)} as communication device.") } } else { val searchOrder: List = listOf(AudioDevice.BLUETOOTH, AudioDevice.WIRED_HEADSET, defaultAudioDevice, AudioDevice.EARPIECE, AudioDevice.SPEAKER_PHONE, AudioDevice.NONE).distinct() @@ -194,14 +253,14 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener when (candidate) { null -> { - Log.e(TAG, "Tried to switch audio devices but could not find suitable device in list of types: ${availableCommunicationDevices.map { it.type }.joinToString()}") + 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() } else -> { - Log.d(TAG, "Switching to new device of type ${candidate.type} from ${currentAudioDevice?.type}") + 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: ${candidate.type}) as communication device.") + Log.w(TAG, "Succeeded in setting ${candidate.id} (type: ${getDeviceTypeName(candidate.type)}) as communication device.") 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.") @@ -210,4 +269,52 @@ class FullSignalAudioManagerApi31(context: Context, eventListener: EventListener } } } + + 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" + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "BUILTIN_SPEAKER" + AudioDeviceInfo.TYPE_BUILTIN_MIC -> "BUILTIN_MIC" + AudioDeviceInfo.TYPE_WIRED_HEADSET -> "WIRED_HEADSET" + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "WIRED_HEADPHONES" + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "BLUETOOTH_SCO" + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "BLUETOOTH_A2DP" + AudioDeviceInfo.TYPE_USB_DEVICE -> "USB_DEVICE" + AudioDeviceInfo.TYPE_USB_ACCESSORY -> "USB_ACCESSORY" + AudioDeviceInfo.TYPE_USB_HEADSET -> "USB_HEADSET" + AudioDeviceInfo.TYPE_TELEPHONY -> "TELEPHONY" + AudioDeviceInfo.TYPE_HEARING_AID -> "HEARING_AID" + AudioDeviceInfo.TYPE_BLE_HEADSET -> "BLE_HEADSET" + AudioDeviceInfo.TYPE_BLE_SPEAKER -> "BLE_SPEAKER" + AudioDeviceInfo.TYPE_BLE_BROADCAST -> "BLE_BROADCAST" + else -> "UNKNOWN($type)" + } + } + + private fun getAudioSourceName(source: Int): String { + return when (source) { + MediaRecorder.AudioSource.DEFAULT -> "DEFAULT" + MediaRecorder.AudioSource.MIC -> "MIC" + MediaRecorder.AudioSource.VOICE_UPLINK -> "VOICE_UPLINK" + MediaRecorder.AudioSource.VOICE_DOWNLINK -> "VOICE_DOWNLINK" + MediaRecorder.AudioSource.VOICE_CALL -> "VOICE_CALL" + MediaRecorder.AudioSource.CAMCORDER -> "CAMCORDER" + MediaRecorder.AudioSource.VOICE_RECOGNITION -> "VOICE_RECOGNITION" + MediaRecorder.AudioSource.VOICE_COMMUNICATION -> "VOICE_COMMUNICATION" + MediaRecorder.AudioSource.UNPROCESSED -> "UNPROCESSED" + MediaRecorder.AudioSource.VOICE_PERFORMANCE -> "VOICE_PERFORMANCE" + else -> "UNKNOWN($source)" + } + } }