1

I am developing an app in which we need to use the headphone jack as a button only.

Requirement : Play the default audio (calling) via earpiece when headsets are connected (no need of audio through headphones)

There are many example of routing audio through speaker and headphones and also bluetooth headsets but nothing about routing the audio through ear speakers of devices if headsets are connected. I have tried a lot and some links are

Android : Force audio routing (not working in my scenario)

I have checked SoundAbout(https://play.google.com/store/apps/details?id=com.woodslink.android.wiredheadphoneroutingfix&hl=en) app and it is routing the audio to various port like headset, speakers and earpieces.

I have got audio to speakers if headsets are connected: Here is my code

if (Build.VERSION.SDK_INT >= 21) {
            ForegroundService.audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
            ForegroundService.audioManager.setSpeakerphoneOn(true);
            SplashScreen.preferences.edit().putBoolean("isKey", true).commit();
        } else {
            Class audioSystemClass = null;
            try {
                audioSystemClass = Class.forName("android.media.AudioSystem");
                Method setForceUse = audioSystemClass.getMethod("setForceUse", int.class, int.class);
                setForceUse.invoke(null, FOR_MEDIA, FORCE_SPEAKER);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }


            SplashScreen.preferences.edit().putBoolean("isKey", true).commit();
            ForegroundService.audioManager.setSpeakerphoneOn(true);
        }
Community
  • 1
  • 1
DevChamp
  • 188
  • 2
  • 2
  • 14

2 Answers2

3

The earpiece is never used for media in Android, and it can only be used if the phone is in "call" or "communication" (VoIP) state.

I guess you have noticed that there is no "FORCE_EARPIECE" constant, so it can't be specified in a call to setForceUse.

Also, the earpiece has the lowest priority in output device selection for calls, so if the phone has anything connected to it (and in your case there is a fake headset), that device will be selected (see https://android.googlesource.com/platform/frameworks/av/+/322b4d2/services/audiopolicy/enginedefault/src/Engine.cpp#381).

Sorry, it doesn't seem to be possible to achieve what you intend.

UPDATE

After examining media.audio_policy state while SoundAbout is enforcing the use of earpiece for media, I have discovered the following tricks that this app uses:

  1. It calls AudioSystem.setPhoneState(MODE_IN_COMMUNICATION) for enforcing "communication" phone state (usually used for VoIP calls).

  2. If a headset (or headphones) is connected, in order to prevent the sound to be routed to it due to higher priority, the app calls AudioSystem.setDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET, DEVICE_STATE_UNAVAILABLE, ...) to trick Audio Manager to believe that there is no headset.

These are all hacks and require the app to monitor the phone state closely. It also doesn't work all the time.

Another drawback is that using earpiece disable on-chip audio decompression and thus has higher battery use.

In general, I wouldn't recommend using these techniques.

Mikhail Naganov
  • 6,643
  • 1
  • 26
  • 26
  • Thanks @Mikhail for sharing your thoughts. I know it is not possible. It looks like Google doesn't want to provide any method to have a direct button for developers. I have researched a lot on this and found that all the methods which can access it has been removed or hidden or depricated. But some apps still doing it like mSwitch, Keycut, iKey on playstore so this is possible but I don't know how but I need this funtionality in my app. I think they are disconnecting the headphone programmatically using reflection then audio get routed to earpiece as usual using priority which you know. – DevChamp Jul 26 '16 at 09:01
  • At least mSwitch (KeyCut) requires rooting your phone. With root access you definitely can change the audio policy configuration to do anything you need. – Mikhail Naganov Jul 26 '16 at 15:40
  • I don't want root access and the devices on which I tested was not rooted. Please check SoundAbout and Kclick app on playstore. Select to route earpiece and you will get audio in the earpiece not in the headphones. – DevChamp Jul 27 '16 at 03:32
  • I've checked SoundAbout (thanks for the pointer!) -- when you select earpiece as an output for media, the app puts the phone into `COMMUNICATION` state (by calling `AudioSystem.setPhoneState` via reflection), as a result they interfere with VoIP apps and need to track what is going on on the phone. Another drawback is that phone hardware doesn't support MP3 / AAC decoding for earpiece, so the decoding happens in software and drains more battery. I'm still not sure how it manages to get output to the earpiece when a headset is connected -- will check that later. – Mikhail Naganov Jul 28 '16 at 01:23
  • And for making earpiece to work when a headset is connected, it seems that SoundAbout uses `AudioSystem.setDeviceConnectionState` to trick the audio policy manager into thinking that there is no headset. That's a gross hack. But I will update my answer with these findings for completeness. – Mikhail Naganov Jul 29 '16 at 00:03
2

After researching a lot, I found it out that there is not any way to achieve this funtionality without using reflection. First you need to put headset jack in and then call the method setWiredDeviceConnectionState() with suitable parameters then it behave like the headphone are disconnected but click works still. So it is a hack but as per my requirement, it's not a foolproof solution but working for now. Here is my code to do this,

private void sendIntent(Intent i) {
        Method m;
        Log.i(TAG, "Device sdk = " + Build.VERSION.SDK_INT);
        try {
            if (Build.VERSION.SDK_INT < 16) {
                Class<?> clazz = Class.forName("android.app.ActivityManagerNative");
                m = clazz.getMethod("broadcastStickyIntent", Intent.class, String.class);
                m.setAccessible(true);
                m.invoke(clazz, i, null);
                return;
            } else if (Build.VERSION.SDK_INT < 23) {
                //int type, int state, String address, String name
                m = am.getClass().getMethod("setWiredDeviceConnectionState", Integer.TYPE, Integer.TYPE, String.class);
                m.setAccessible(true);
                Object[] objArr = new Object[3];
                objArr[0] = (i.getIntExtra("microphone", 0) == 0) ? 8 : 4;
                objArr[1] = i.getIntExtra("state", 0);
                objArr[2] = i.getStringExtra("name");
                m.invoke(am, objArr);
            } else {
                //int type, int state, String address, String name
                m = am.getClass().getMethod("setWiredDeviceConnectionState", Integer.TYPE, Integer.TYPE, String.class, String.class);
                m.setAccessible(true);
                Object[] objArr = new Object[4];
                objArr[0] = (i.getIntExtra("microphone", 0) == 0) ? 8 : 4;
                objArr[1] = i.getIntExtra("state", 0);
                objArr[2] = i.getStringExtra("address");
                objArr[3] = i.getStringExtra("name");
                m.invoke(am, objArr);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

the intent to send :

@TargetApi(Build.VERSION_CODES.M)
public class HeadSetJackReciever extends AudioDeviceCallback {
    public static boolean isAudioChecked;

    public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
        if (addedDevices.length != 0) {
            for (int i = 0; i < addedDevices.length; i++) {
                if (addedDevices[i].getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
                    AudioDeviceInfo audioDeviceInfo = addedDevices[i];
                    int microphone = audioDeviceInfo.getType();
                    String headsetName = "DCS";
                    String headsetAddress = "";
                    try {
                        Method method = audioDeviceInfo.getClass().getMethod("getAddress");
                        method.setAccessible(true);
                        headsetAddress = (String) method.invoke(audioDeviceInfo);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    Log.e("TEST", "microphone:"+microphone);
                    Log.e("TEST", "headsetName:"+headsetName);
                    Log.e("TEST", "headsetAddress:"+headsetAddress );
                    Intent intent = new Intent(ForegroundService.context, SelectAudioOutput.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("microphone",microphone);
                    intent.putExtra("headsetName",headsetName);
                    intent.putExtra("headsetAddress",headsetAddress);
                    ForegroundService.context.startActivity(intent);
                }
            }
        }
    }

    public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
        if (removedDevices.length != 0) {
            Log.e("TEST", "Audio deinserted");
            if (SplashScreen.preferences.getBoolean("isKey", false)) {
                Intent startIntent = new Intent(ForegroundService.context, ForegroundService.class);
                startIntent.setAction(Constants.ACTION.STARTNOTIFICATION_ACTION);
                ForegroundService.context.startService(startIntent);
            } else {
                Intent startIntent = new Intent(ForegroundService.context, ForegroundService.class);
                startIntent.setAction(Constants.ACTION.STOPNOTIFICATION_ACTION);
                ForegroundService.context.startService(startIntent);
            }
            ForegroundService.audioManager.setMode(AudioManager.MODE_IN_CALL);
            ForegroundService.audioManager.setSpeakerphoneOn(false);
        }
    }
}

for Lollipop and lower versions :

if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
            headsetName = intent.getStringExtra("name");
            microphone = intent.getIntExtra("microphone", 0);

            int state = intent.getIntExtra("state", -1);
            switch (state) {
                case 0:
                    Log.d("onReceive", "Headset unplugged");
                    Log.e("TEST", "Audio deinserted");
                    if (SplashScreen.preferences.getBoolean("isKey", false)) {
                        Intent startIntent = new Intent(ForegroundService.context, ForegroundService.class);
                        startIntent.setAction(Constants.ACTION.STARTNOTIFICATION_ACTION);
                        context.startService(startIntent);
                    } else {
                        Intent startIntent = new Intent(ForegroundService.context, ForegroundService.class);
                        startIntent.setAction(Constants.ACTION.STOPNOTIFICATION_ACTION);
                        context.startService(startIntent);
                    }
                    ForegroundService.audioManager.setMode(AudioManager.MODE_IN_CALL);
                    ForegroundService.audioManager.setSpeakerphoneOn(false);
                    break;
                case 1:

                    Log.d("onReceive", "Headset plugged");
                    Log.e("TEST", "microphone:"+microphone);
                    Log.e("TEST", "headsetName:"+headsetName);
                    Log.e("TEST", "headsetAddress:"+headsetAddress );
                    Intent intentone = new Intent(ForegroundService.context, SelectAudioOutput.class);
                    intentone.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intentone.putExtra("microphone",microphone);
                    intentone.putExtra("headsetName",headsetName);
                    intentone.putExtra("headsetAddress",headsetAddress);
                    context.startActivity(intentone);
                    break;
            }
        }

Let me know if I miss something. Thanks.

DevChamp
  • 188
  • 2
  • 2
  • 14
  • in HeadsetJackReceiver addedDevices[ ] array i'm getting 2 headsets & 1 headphone. So the code sends two intents and audio isn't routed. I'm using Android M(23). Can you help? – Prasad Pawar Mar 22 '17 at 10:15
  • @DCS in oder to integrate this logic in my program, i would request you to provide short information on `ForegroundService`, `SelectAudioOutput`, `SplashScreen` & `Constants` are suppose to do ? – Vikrant Jan 15 '19 at 05:41