[Android] Force route audio stream to wired headset

I want to force route audio stream to headphone for some tests, no matter headset is on or off.

API has been deprecrated

Andoird's AudioManager has such API before, called setRouting , but it has deprecated for quite a long time. Currently, the available APIs can't meet my demands.

public void setRouting (int mode, int routes, int mask)
This method was deprecated in API level 4. Do not set audio routing directly, use setSpeakerphoneOn(), setBluetoothScoOn() methods instead.

Way out through hacking

But if we looks at the source code about andorid.media system, it still opens a door to archive this, needs some hacking.

The idea

When wired headset is plugged, the audio stream will route to it. So if we look at how Andoird handle this event, we can follow the same way to implement force route.

How Android handle these audio routing?

The AudioManager is the public interface exposed to Android SDK, but the inversivle hand which do the most work is AudioService and AudioSystem.

In AudioService, I found such code to enable Bluetooth, note the line 2 is the key to enable audio device.

1
2
3
4
5
6
7
    private void makeA2dpSrcAvailable(String address) {
        AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP,
                AudioSystem.DEVICE_STATE_AVAILABLE,
                address);
        mConnectedDevices.put( new Integer(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP),
                address);
    }
The hidden API

So look at the this API in AudioSystem, it's a native API, we don't care about too much detail here.

1
    public static native int setDeviceConnectionState(int device, int state, String device_address);

Here is available INPUT/OUTPUT device defined in AudioSystem, corresponding to the first parameter (int device) in above API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    // output devices, be sure to update AudioManager.java also
    public static final int DEVICE_OUT_EARPIECE = 0x1;
    public static final int DEVICE_OUT_SPEAKER = 0x2;
    public static final int DEVICE_OUT_WIRED_HEADSET = 0x4;
    public static final int DEVICE_OUT_WIRED_HEADPHONE = 0x8;
    public static final int DEVICE_OUT_BLUETOOTH_SCO = 0x10;
    public static final int DEVICE_OUT_BLUETOOTH_SCO_HEADSET = 0x20;
    public static final int DEVICE_OUT_BLUETOOTH_SCO_CARKIT = 0x40;
    public static final int DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
    public static final int DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100;
    public static final int DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200;
    public static final int DEVICE_OUT_AUX_DIGITAL = 0x400;
    public static final int DEVICE_OUT_HDMI = DEVICE_OUT_AUX_DIGITAL;
    public static final int DEVICE_OUT_ANLG_DOCK_HEADSET = 0x800;
    public static final int DEVICE_OUT_DGTL_DOCK_HEADSET = 0x1000;
    public static final int DEVICE_OUT_USB_ACCESSORY = 0x2000;
    public static final int DEVICE_OUT_USB_DEVICE = 0x4000;
    public static final int DEVICE_OUT_REMOTE_SUBMIX = 0x8000;
    public static final int DEVICE_OUT_TELEPHONY_TX = 0x10000;
    public static final int DEVICE_OUT_LINE = 0x20000;
    public static final int DEVICE_OUT_HDMI_ARC = 0x40000;
    public static final int DEVICE_OUT_SPDIF = 0x80000;
    public static final int DEVICE_OUT_FM = 0x100000;
    public static final int DEVICE_OUT_AUX_LINE = 0x200000;

    // input devices
    public static final int DEVICE_IN_COMMUNICATION = DEVICE_BIT_IN | 0x1;
    public static final int DEVICE_IN_AMBIENT = DEVICE_BIT_IN | 0x2;
    public static final int DEVICE_IN_BUILTIN_MIC = DEVICE_BIT_IN | 0x4;
    public static final int DEVICE_IN_BLUETOOTH_SCO_HEADSET = DEVICE_BIT_IN | 0x8;
    public static final int DEVICE_IN_WIRED_HEADSET = DEVICE_BIT_IN | 0x10;
    public static final int DEVICE_IN_AUX_DIGITAL = DEVICE_BIT_IN | 0x20;
    public static final int DEVICE_IN_HDMI = DEVICE_IN_AUX_DIGITAL;
    public static final int DEVICE_IN_VOICE_CALL = DEVICE_BIT_IN | 0x40;
    public static final int DEVICE_IN_TELEPHONY_RX = DEVICE_IN_VOICE_CALL;
    public static final int DEVICE_IN_BACK_MIC = DEVICE_BIT_IN | 0x80;
    public static final int DEVICE_IN_REMOTE_SUBMIX = DEVICE_BIT_IN | 0x100;
    public static final int DEVICE_IN_ANLG_DOCK_HEADSET = DEVICE_BIT_IN | 0x200;
    public static final int DEVICE_IN_DGTL_DOCK_HEADSET = DEVICE_BIT_IN | 0x400;
    public static final int DEVICE_IN_USB_ACCESSORY = DEVICE_BIT_IN | 0x800;
    public static final int DEVICE_IN_USB_DEVICE = DEVICE_BIT_IN | 0x1000;
    public static final int DEVICE_IN_FM_TUNER = DEVICE_BIT_IN | 0x2000;
    public static final int DEVICE_IN_TV_TUNER = DEVICE_BIT_IN | 0x4000;
    public static final int DEVICE_IN_LINE = DEVICE_BIT_IN | 0x8000;
    public static final int DEVICE_IN_SPDIF = DEVICE_BIT_IN | 0x10000;
    public static final int DEVICE_IN_BLUETOOTH_A2DP = DEVICE_BIT_IN | 0x20000;
    public static final int DEVICE_IN_LOOPBACK = DEVICE_BIT_IN | 0x40000;
    public static final int DEVICE_IN_DEFAULT = DEVICE_BIT_IN | DEVICE_BIT_DEFAULT;

For the parameter int state, you can set it to (un)available.

1
2
3
    // device states, must match AudioSystem::device_connection_state
    public static final int DEVICE_STATE_UNAVAILABLE = 0;
    public static final int DEVICE_STATE_AVAILABLE = 1;

The last parameter String device_address, it's intend for device like Bluetooth which has associated address, for headset, we can just pass an empty String to it.

So we can do various force routing no limited to wired headset, let's get started.

Implements

Here is a sample code to force route audio stream to wired headset.

Note there is a trick that we can't easily access AudioSystem since it's not exposed in Android SDK. Here we use reflection in Java, you can see from line 16 ~ 20.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import android.util.Log;
import java.lang.reflect.Method;

public class AudioTest {
    private final String TAG = "AudioTest";
     // Constants copied from AudioSystem
    private static final int DEVICE_IN_WIRED_HEADSET    = 0x400000;
    private static final int DEVICE_OUT_EARPIECE        = 0x1;
    private static final int DEVICE_OUT_WIRED_HEADSET   = 0x4;
    private static final int DEVICE_STATE_UNAVAILABLE   = 0;
    private static final int DEVICE_STATE_AVAILABLE     = 1;

    /* force route function through AudioSystem */
    private void setDeviceConnectionState(final int device, final int state, final String address) {
        try {
            Class<?> audioSystem = Class.forName("android.media.AudioSystem");
            Method setDeviceConnectionState = audioSystem.getMethod(
                    "setDeviceConnectionState", int.class, int.class, String.class);

            setDeviceConnectionState.invoke(audioSystem, device, state, address);
        } catch (Exception e) {
            Log.e(TAG, "setDeviceConnectionState failed: " + e);
        }
    }

    public void forceRouteHeadset(boolean enable) {
        if (enable) {
            Log.i(TAG, "force route to Headset");
            setDeviceConnectionState(DEVICE_IN_WIRED_HEADSET, DEVICE_STATE_AVAILABLE, "");
            setDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET, DEVICE_STATE_AVAILABLE, "");
        } else {
            Log.i(TAG, "force route to Earpirce");
            setDeviceConnectionState(DEVICE_IN_WIRED_HEADSET, DEVICE_STATE_UNAVAILABLE, "");
            setDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET, DEVICE_STATE_UNAVAILABLE, "");
            setDeviceConnectionState(DEVICE_OUT_EARPIECE, DEVICE_STATE_AVAILABLE, "");
        }
    }
}

Reference

  1. http://stackoverflow.com/questions/6834816/is-there-any-way-to-force-audio-through-the-speakers-when-headphones-are-plugged
  2. http://forum.xda-developers.com/showthread.php?t=1472440
  3. https://code.google.com/p/toggleheadset2/

留言