Search code examples
androidandroid-audiomanagerandroid-12

AudioManager auto switching own mode + not respecting setSpeakerphoneOn()


I want to play some audio with volume lvl adjusted to ear aka. "phone call mode". For this purpose, I'm using well-known and commonly advised

audioManager.setMode(audioNormalState ?
            AudioManager.MODE_NORMAL : AudioManager.MODE_IN_COMMUNICATION);

The problem is that I'm not always playing audio just after mode switching, I have to wait and can't be sure how long, it may be even minutes. I've made some looped logging and MODE_IN_COMMUNICATION mode is kept as long as the user is in "phone call mode" in my app on some Motorola with Android 9, but on Pixel 3 with Android 12 after 6 seconds mode is auto-switching back to MODE_NORMAL when nothing is played. No additional code executed (like some listener), no additional (system) logs. When I start playing audio 1 sec after switching to MODE_IN_COMMUNICATION mode it won't auto-switch as long as the audio is played (even more than 6 secs), but right after finishing mode gets also auto-switched to MODE_NORMAL.

My app makes sort-of real-time voice calls (commands), but can also "beep" a few signals-patterns, and there is also a history feature providing all chronological sound-making actions to be played again in order. If this would be only voice then switching to MODE_IN_COMMUNICATION and back only during a call might be sufficient, but what to do with highly-important SoundPool jingles, do I have to switch mode also for them? (or for history play, which is a mix) AFAIK mode switching isn't fast (even a few secs on some devices), so I may apply some significant delay for short hundreds-of-ms signals (no way, every ms is crucial!) or I'm risking playing out loud signal/voice even in "phone call mode", when mode doesn't change "fast enough" (user won't be happy). I was relying on setting fixed (but configured according to app state and settings) MODE_IN_COMMUNICATION, which was working till Android 12... (can confirm new/wrong behavior on Pixels and Samsungs)

currently used method for switching audio mode + configuration below, worth noting that setSpeakerphoneOn method also doesn't work always on Android 12. At least not when on MODE_NORMAL, which is default auto-switch-back-to mode now, also isSpeakerphoneOn is false on the very first start, but all my audio sources are in fact played loud...

// forceAudioNormalState = true only when app exit!
public static void resolveLoudState(AudioManager audioManager, boolean forceAudioNormalState) {
    boolean silentPhoneCallMode = isPhoneCallModeEnabled(); // phone call GUI, only ear-friendly volume!!
    boolean silentHeadset = HeadsetPlugReceiver.isHeadsetPlugged &&
            !HeadsetPlugReceiver.forceSpeakerWhenHeadsetOn; // headset plugged, but "muted", force speaker
    boolean silentBluetooth = BluetoothController.isAudioDeviceConnected() &&
            !audioManager.isBluetoothScoOn(); // bt headset plugged, but "muted", force speaker

    boolean loud = true; // by default
    if (silentPhoneCallMode || silentHeadset || silentBluetooth) loud = false;

    String log = String.format("resolveLoudState play loud: %s," +
                    " silentPhoneCallMode: %s, silentHeadset: %s, silentBluetooth: %s",
            loud, silentPhoneCallMode, silentHeadset, silentBluetooth);
    Timber.i(log);

    audioManager.setMode(forceAudioNormalState ?
            AudioManager.MODE_NORMAL : AudioManager.MODE_IN_COMMUNICATION);
    audioManager.setSpeakerphoneOn(loud);
    // even if deprecated this still works! fake wired headset on even for bt
    audioManager.setWiredHeadsetOn(!loud && (silentHeadset || silentBluetooth));
    // not loud and any headset connected and "muted"
}

note that in the above snippet there is no flag/information about the current playing state, only apps state/config

I want to manage these modes by myself and decide which audio output will be used or maybe there is any other way for forcing playing all audio (AudioTrack, SoundPool, MediaPlayer, ExoPlayer etc.) with ear-friendly adjusted volume?

edit: Just noticed when the mode auto-switches to MODE_NORMAL and I will start playing STREAM_VOICE_CALL it will auto switch to MODE_IN_COMMUNICATION (with some small but significant delay) and reset back again just after finishing audio... This is some new undocumented behavior of system overall, become very unfriendly, bugged and unclear API...

edit2: this looks like related issue

PS. I've noticed that MediaSession apps (e.g. music players) on Android 12 device got a new option straight on Notification for picking speaker/headphones when wired/bt headset/earphones connected, but I'm not using session API at all. bonus question: is there an API for that?


Solution

  • found some answers to my own question, sharing with community

    6-sec auto-switch mode is a new feature in Android 12, which works only if (mode == AudioSystem.MODE_IN_COMMUNICATION) (check out flow related to MSG_CHECK_MODE_FOR_UID flag). This should help for MODE_IN_COMMUNICATION set to AudioManager and left after app exit, this was messing with global/system-level audio routing. There is also a brand new AudioManager.OnModeChangedListener called when mode is (auto-)changing

    and setSpeakerphoneOn turns out to be deprecated, even if this isn't marked in doc... we have new method setCommunicationDevice(AudioDeviceInfo) and in its description we have info about startBluetoothSco(), stopBluetoothSco() and setSpeakerphoneOn(boolean) deprecation. I'm using all three methods and now on Android 12 I'm iterating through getAvailableCommunicationDevices(), comparing type of every item and if desired type found I'm calling setCommunicationDevice(targetAudioDeviceInfo). I'm NOT switching audio mode at all now, staying on MODE_NORMAL. All my streams are AudioManager.STREAM_VOICE_CALL type (where applicable)

    for built-in earpiece audio playback aka. "ear-friendly mode" we were using

    if (earpieceMode) {
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        audioManager.setSpeakerphoneOn(false); // call AFTER setMode
    }
    

    that won't work reliable on Android 12 (after multiple speakerphone state switches). Now I'm using below code (comprehensive snippet)

    ArrayList<Integer> targetTypes = new ArrayList<>();
    //add types according to needs, may be few in order of importance
    if (bluetoothScoConnected) {
        targetTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
    } else if (wiredHeadsetConnected) {
        if (isUsbHeadset) {
            targetTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET);
            targetTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE);
            targetTypes.add(AudioDeviceInfo.TYPE_USB_ACCESSORY);
        } else {
            targetTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
            targetTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
        }
    } else if (earpieceMode) {
        targetTypes.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
    } else { // play out loud
        targetTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
    }
    
    Boolean result = null;
    List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
    
    outer:
    for (Integer targetType : targetTypes) {
        for (AudioDeviceInfo device : devices) {
            if (device.getType() == targetType) {
                result = audioManager.setCommunicationDevice(device);
                Log.i("AUDIO_MANAGER", "setCommunicationDevice type:" + targetType + " result:" + result);
                break outer;
            }
        }
    }
    
    if (result == null) {
        Log.i("AUDIO_MANAGER", "setCommunicationDevice targetType NOT FOUND!!");
    }
    

    worth mentioning Bluetooth SCO headset case - when freshly connected/paired with device all my accessories are recognized as AudioDeviceInfo.TYPE_BLUETOOTH_A2DP type (getCommunicationDevice()). I do want SCO, which isn't listed in getAvailableCommunicationDevices() for few seconds after A2DP connection, so I'm leaving some countdown timer, which checks (interval 2s) and waits for AudioDeviceInfo.TYPE_BLUETOOTH_SCO for few secs (I've set 16), then I'm switching to this type when appear on list or just dismissing timer