10

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?

G H Prakash
  • 1,720
  • 10
  • 30
snachmsm
  • 17,866
  • 3
  • 32
  • 74

2 Answers2

7

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

snachmsm
  • 17,866
  • 3
  • 32
  • 74
  • ```am.setCommunicationDevice(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)``` not working for me. – Sujith S Manjavana Sep 29 '22 at 08:08
  • are you shure that your device has earpiece? is it listed in `getAvailableCommunicationDevices()` return? – snachmsm Sep 29 '22 at 09:37
  • Yes, it has an earpiece and it's a pixel phone. And I'm using ```AudioManager.MODE_IN_COMMUNICATION``` – Sujith S Manjavana Sep 29 '22 at 09:48
  • well, I'm staying with `MODE_NORMAL` in such case (but my audio is `AudioManager.STREAM_VOICE_CALL`) – snachmsm Sep 29 '22 at 09:54
  • Is that okay to use ```NORMAL_MODE``` for voice calls? What difference will it make to the app's behavior if we set the wrong mode? – Sujith S Manjavana Sep 29 '22 at 11:30
  • 1
    I would bet that `NORMAL_MODE` app can be "muted" by other higher-audio-priority app, e.g. that one which is using `MODE_IN_COMMUNICATION` ;) also some audio sounds made by deice, e.g. made with `SoundPool` can be played out loud, probably even if app is switched to earpiece. but for realiably-working earpiece mode in my app I've stayed with above solution, one and onlcy case when switching to earpiece worked on newest OS versions – snachmsm Sep 29 '22 at 11:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248448/discussion-between-sujith-s-manjavana-and-snachmsm). – Sujith S Manjavana Sep 29 '22 at 14:22
  • I tried it without ```MODE_IN_COMMUNICATION``` but the audio is still playing in loudspeaker. – Sujith S Manjavana Sep 29 '22 at 18:24
  • in my case `TYPE_BUILTIN_SPEAKER` gives **loud**speaker, `TYPE_BUILTIN_EARPIECE` gives also loudspeaker, but audio volume is adjusted to keeping near to ear (still its probably same physical unit). works like intended for me (I do make some voice communication, but also some "signals", which also should be considered as information, an example would be a Morse code). would advice to post own question with more details, at least how do you perfom streaming and audio generating – snachmsm Sep 30 '22 at 12:14
  • Could you please check it => https://stackoverflow.com/questions/73910789/android-12-audiomanager-setcommunicationdevice-not-working – Sujith S Manjavana Sep 30 '22 at 15:14
  • ```NORMAL_MODE``` is not good for calls as the microphone volume will be very low. – Sujith S Manjavana Oct 04 '22 at 16:04
  • Google tells that the bug was fixed in Chromium version M106. Does anyone know if the the fix is included in any Android 12 or 13 version? – Juha Feb 14 '23 at 22:48
-1
public void setAudioPlayByEarPhone(int state) {
    AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    if (state == 0) {
        audioManager.setMode(AudioManager.MODE_NORMAL);
        audioManager.setSpeakerphoneOn(true);
    } else if (state == 1) {
        audioManager.setSpeakerphoneOn(false);
    } else {
        audioManager.setSpeakerphoneOn(false);
        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
        int currVolume = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
        audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, currVolume, AudioManager.STREAM_VOICE_CALL);
    }
}

for earpiece use setAudioPlayByEarPhone(3) for speaker use setAudioPlayByEarPhone(0) onDestroy() use setAudioPlayByEarPhone(1)