17

I’m developing an Android application and I want it to interact with headset button clicks. I’m testing it in a Nexus 5 with Android KitKat 4.4.

I tried first with a simple headset (not wireless). The button event received was KEYCODE_HEADSETHOOK (79). I created a MEDIA_BUTTON receiver to handle its clicks:

<receiver android:name="com.example.mytest.SearchActivity$MediaButtonIntentReceiver">
    <intent-filter>
        <intent-filter android:priority="1000000000">
            <action android:name="android.intent.action.MEDIA_BUTTON" />
        </intent-filter>
    </intent-filter>
</receiver>

This is the activity holding the receiver:

public class SearchActivity extends Activity {

    private AudioManager mAudioManager;
    private ComponentName mAudioReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.search);

        mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
        mAudioReceiver =  new ComponentName(getPackageName(),
            MediaButtonIntentReceiver.class.getName());
    }

    @Override
    protected void onResume() {
        super.onResume();

        mAudioManager.registerMediaButtonEventReceiver(mAudioReceiver);
    }

    @Override
    protected void onPause() {
        super.onPause();

        mAudioManager.unregisterMediaButtonEventReceiver(mAudioReceiver);
    }

    public static class MediaButtonIntentReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("SA", "ON RECEIVE");

            ...

            abortBroadcast();
        }
    }
}

This code works with my wired headset only if the user performs a short click. Performing a long click opens Google Voice Search. I’d like to capture long clicks too, but I don’t mind if it’s not possible.

After that I tested it with a bluetooth headset. Specifically, I’m using Moveteck Bluetooth Headset BH119A (you can see an image at the bottom of this post). This headset only has one button, and if I press it the following "activity" is opened:

enter image description here

I’d like to capture this click event too if my Activity is opened. How can I do it? I tried adding the following filters to my receiver, but it’s not working either:

<action android:name="android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT" />
<action android:name="android.intent.action.VOICE_COMMAND" />
<action android:name="android.intent.action.CALL_BUTTON" />

I also tried overriding onKeyDown in my Activity, but it’s not triggered.

Anybody knows how can I intercept those events?

This is my bluetooth headset:

enter image description here


** EDIT **

Following Toaster’s advice, I checked the whole log looking for the events my headset triggers.

Wired headset long click

This is the log when I long click in my wired headset (it opens Google Voice Search):

12-10 09:24:36.644: I/MediaFocusControl(740): voice-based interactions: about to use ACTION_WEB_SEARCH
12-10 09:24:36.644: I/ActivityManager(740): START u0 {act=android.speech.action.WEB_SEARCH flg=0x10800000 cmp=com.google.android.googlequicksearchbox/.SearchActivity} from pid 740
12-10 09:24:36.754: I/ActivityManager(740): START u0 {act=android.speech.action.WEB_SEARCH flg=0x10000000 cmp=com.google.android.googlequicksearchbox/com.google.android.launcher.GEL} from pid 10153
12-10 09:24:36.764: I/InputDispatcher(740): Dropping event because there is no focused window or focused application.
12-10 09:24:36.764: I/InputDispatcher(740): Dropping event because there is no focused window or focused application.
12-10 09:24:36.774: I/GEL(1025): handleIntent(Intent { act=android.speech.action.WEB_SEARCH flg=0x10400000 cmp=com.google.android.googlequicksearchbox/com.google.android.launcher.GEL })
12-10 09:24:36.774: V/SearchControllerCache(10153): creating SearchController
12-10 09:24:36.804: I/AudioRouter(10153): ROUTE_NONE->ROUTE_NO_BLUETOOTH
12-10 09:24:36.804: I/MediaFocusControl(740):  AudioFocus  requestAudioFocus() from android.media.AudioManager@4267ad58com.google.android.voicesearch.audio.AudioRouterImpl$1@42695f60
12-10 09:24:36.804: I/Velvet.SdchManager(10153): Sdch cache load complete.
12-10 09:24:36.814: W/IInputConnectionWrapper(18407): showStatusIcon on inactive InputConnection
12-10 09:24:36.814: I/Icing.InternalIcingCorporaProvider(10153): Updating corpora: A: NONE, C: DELTA
12-10 09:24:36.854: I/VS.G3EngineManager(10153): create_rm: m=GRAMMAR,l=en-US
12-10 09:24:36.854: W/Search.ConcurrentUtils(10153): Executor queue length is now 9. Perhaps some tasks are too long, or the pool is too small. [GrecoExecutor-1]
12-10 09:24:36.854: I/VS.G3EngineManager(10153): Brought up new g3 instance :/system/usr/srec/en-US/grammar.config for: en-USin: 9 ms
12-10 09:24:36.864: D/audio_hw_primary(189): out_set_parameters: enter: usecase(1: low-latency-playback) kvpairs: routing=4
12-10 09:24:36.864: D/audio_hw_primary(189): select_devices: out_snd_device(4: headphones) in_snd_device(0: )
12-10 09:24:36.874: D/audio_hw_primary(189): select_devices: out_snd_device(0: ) in_snd_device(18: headset-mic)
12-10 09:24:36.874: D/(189): Failed to fetch the lookup information of the device 00000008 
12-10 09:24:36.874: E/ACDB-LOADER(189): Error: ACDB AudProc vol returned = -19
12-10 09:24:38.864: I/LATENCY(10153): 0-4,45-2064,
12-10 09:24:38.874: I/AudioRouter(10153): ROUTE_NO_BLUETOOTH->ROUTE_NONE
12-10 09:24:38.874: I/MediaFocusControl(740):  AudioFocus  abandonAudioFocus() from android.media.AudioManager@4267ad58com.google.android.voicesearch.audio.AudioRouterImpl$1@42695f60
12-10 09:24:38.874: I/MicrophoneInputStream(10153): mic_close

It seems it triggers an ACTION_WEB_SEARCH event, so I tried to add it to the filter. I tried it two ways:

  1. Declaring the filter in the manifest:

    <action android:name="android.intent.action.WEB_SEARCH" />
    
  2. Declaring the filter programmatically:

    protected void onResume() {
        IntentFilter f = new IntentFilter(Intent.ACTION_WEB_SEARCH);
        registerReceiver(myReceiver, f);
    }
    
    private BroadcastReceiver myReceiver = new BroadcastReceiver() {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("AA", "ON RECEIVE");
        }
    };
    

None of this options work. As I said, this scenario isn’t of much importance, I can deal with it.


Wireless headset simple click

The Wireless headset simple click is the one opening the Voice Dialer and it’s the event I really need to capture. This is the log output:

12-10 10:41:22.014: E/bt-rfcomm(21800): PORT_DataInd, p_port:0x7507a7e8, p_data_co_callback is null
12-10 10:41:22.014: D/HeadsetStateMachine(21800): processVrEvent: state=1 mVoiceRecognitionStarted: false mWaitingforVoiceRecognition: false isInCall: false
12-10 10:41:22.014: I/ActivityManager(740): START u0 {act=android.intent.action.VOICE_COMMAND flg=0x10000000 cmp=com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeIntentActivity} from pid 21800
12-10 10:41:22.154: V/Avrcp(21800): New genId = 440, clearing = 1
12-10 10:41:22.154: D/HandsFreeIntentActivity(10153): #onStart(Intent { act=android.intent.action.VOICE_COMMAND flg=0x10800000 cmp=com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeIntentActivity })
12-10 10:41:22.154: D/HandsFreeIntentActivity(10153): Starting activity: Intent { act=android.intent.action.VOICE_COMMAND flg=0x10000000 cmp=com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeActivity }
12-10 10:41:22.154: I/ActivityManager(740): START u0 {act=android.intent.action.VOICE_COMMAND flg=0x10000000 cmp=com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeActivity} from pid 10153
12-10 10:41:22.204: D/OpenGLRenderer(10153): Enabling debug mode 0
12-10 10:41:22.214: W/IInputConnectionWrapper(18895): showStatusIcon on inactive InputConnection
12-10 10:41:22.244: I/ActivityManager(740): Displayed com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeActivity: +80ms (total +89ms)
12-10 10:41:22.374: I/AudioRouter(10153): ROUTE_NONE->ROUTE_BLUETOOTH_WANTED
12-10 10:41:22.384: I/MediaFocusControl(740):  AudioFocus  requestAudioFocus() from android.media.AudioManager@4267ad58com.google.android.voicesearch.audio.AudioRouterImpl$1@42695f60
12-10 10:41:22.384: V/Avrcp(21800): New genId = 441, clearing = 1
12-10 10:41:22.384: D/BluetoothManagerService(740): Message: 30
12-10 10:41:22.384: D/BluetoothHeadset(10153): Proxy object connected
12-10 10:41:22.384: I/BluetoothController(10153): BT device connected
12-10 10:41:22.394: I/AudioRouter(10153): BT required, starting SCO
12-10 10:41:22.394: I/BluetoothController(10153): Starting VR
12-10 10:41:22.394: D/BluetoothHeadset(10153): startVoiceRecognition()
12-10 10:41:22.394: D/HeadsetStateMachine(21800): Voice recognition started successfully
12-10 10:41:22.394: D/HeadsetStateMachine(21800): Initiating audio connection for Voice Recognition
12-10 10:41:22.394: W/bt-btm(21800): BTM Remote does not support 3-EDR eSCO
12-10 10:41:22.434: I/TextToSpeech(10153): Sucessfully bound to com.google.android.tts
12-10 10:41:22.454: I/TextToSpeech(10153): Connected to ComponentInfo{com.google.android.tts/com.google.android.tts.service.GoogleTTSService}
12-10 10:41:22.454: I/TextToSpeech(10153): Set up connection to ComponentInfo{com.google.android.tts/com.google.android.tts.service.GoogleTTSService}
12-10 10:41:22.484: D/dalvikvm(21966): GC_CONCURRENT freed 346K, 3% free 16647K/17064K, paused 2ms+3ms, total 13ms
12-10 10:41:22.764: D/audio_hw_primary(189): out_set_parameters: enter: usecase(1: low-latency-playback) kvpairs: routing=32
12-10 10:41:22.774: D/audio_hw_primary(189): select_devices: out_snd_device(11: bt-sco-headset) in_snd_device(0: )
12-10 10:41:24.874: I/EventLogService(1148): Aggregate from 1386666683008 (log), 1386666683008 (data)
12-10 10:41:24.994: I/ServiceDumpSys(1148): dumping service [account]
12-10 10:41:25.994: D/dalvikvm(10153): GC_CONCURRENT freed 1582K, 15% free 23868K/27920K, paused 5ms+7ms, total 60ms
12-10 10:41:26.014: I/VS.G3EngineManager(10153): create_rm: m=GRAMMAR,l=en-US
12-10 10:41:26.024: I/VS.G3EngineManager(10153): Brought up new g3 instance :/system/usr/srec/en-US/grammar.config for: en-USin: 4 ms
12-10 10:41:26.024: D/audio_hw_primary(189): out_set_parameters: enter: usecase(1: low-latency-playback) kvpairs: routing=32
12-10 10:41:26.034: D/audio_hw_primary(189): select_devices: out_snd_device(0: ) in_snd_device(25: bt-sco-mic)
12-10 10:41:26.034: D/(189): Failed to fetch the lookup information of the device 00000015 
12-10 10:41:26.034: E/ACDB-LOADER(189): Error: ACDB AudProc vol returned = -19

This time it seems it’s sending an ACTION_VOICE_COMMAND, so I tried to add it to the filter. I tried it two ways:

  1. Declaring the filter in the manifest:

    <action android:name="android.intent.action.VOICE_COMMAND" />
    
  2. Declaring the filter programmatically:

    protected void onResume() {
        IntentFilter f = new IntentFilter(Intent.ACTION_VOICE_COMMAND);
        registerReceiver(myReceiver, f);
    }
    
    private BroadcastReceiver myReceiver = new BroadcastReceiver() {
    
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("AA", "ON RECEIVE");
        }
    };
    

Again, I’m not receiving these events, I don’t know why.

Palec
  • 12,743
  • 8
  • 69
  • 138
PX Developer
  • 8,065
  • 7
  • 42
  • 66

3 Answers3

10

I finally managed to detect the events. I didn't know about this class:

http://developer.android.com/reference/android/bluetooth/BluetoothHeadset.html

Using the classes BluetoothAdapter, BluetoothHeadset and BluetoothDevice I can register a receiver using IntentFilter BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED and I'm able to detect clicks on my headset.

The problem with this is that the broadcast is non-ordered, so I can't abort it. I can close the VoiceDialer activity right after it opens, but that's not what I want.

I'll keep struggling with this.

Thank you @Toaster for your efforts :)

EDIT:

Code used to detect the events:

protected BluetoothAdapter mBluetoothAdapter;
protected BluetoothHeadset mBluetoothHeadset;
protected BluetoothDevice mConnectedHeadset;
protected AudioManager mAudioManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

    if (mBluetoothAdapter != null)
    {

        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        if (mAudioManager.isBluetoothScoAvailableOffCall())
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
            {
                mBluetoothAdapter.getProfileProxy(this, mHeadsetProfileListener, BluetoothProfile.HEADSET);
            }
        }
    }
}

 protected BluetoothProfile.ServiceListener mHeadsetProfileListener = new BluetoothProfile.ServiceListener()
{

    /**
     * This method is never called, even when we closeProfileProxy on onPause.
     * When or will it ever be called???
     */
    @Override
    public void onServiceDisconnected(int profile)
    {
        mBluetoothHeadset.stopVoiceRecognition(mConnectedHeadset);
        unregisterReceiver(mHeadsetBroadcastReceiver);
        mBluetoothHeadset = null;
    }

    @Override
    public void onServiceConnected(int profile, BluetoothProfile proxy)
    {
        // mBluetoothHeadset is just a head set profile, 
        // it does not represent a head set device.
        mBluetoothHeadset = (BluetoothHeadset) proxy;

        // If a head set is connected before this application starts,
        // ACTION_CONNECTION_STATE_CHANGED will not be broadcast. 
        // So we need to check for already connected head set.
        List<BluetoothDevice> devices = mBluetoothHeadset.getConnectedDevices();
        if (devices.size() > 0)
        {
            // Only one head set can be connected at a time, 
            // so the connected head set is at index 0.
            mConnectedHeadset = devices.get(0);

            String log;

            // The audio should not yet be connected at this stage.
            // But just to make sure we check.
            if (mBluetoothHeadset.isAudioConnected(mConnectedHeadset))
            {
                log = "Profile listener audio already connected"; //$NON-NLS-1$     
            }
            else
            {
                // The if statement is just for debug. So far startVoiceRecognition always 
                // returns true here. What can we do if it returns false? Perhaps the only
                // sensible thing is to inform the user.
                // Well actually, it only returns true if a call to stopVoiceRecognition is
                // call somewhere after a call to startVoiceRecognition. Otherwise, if 
                // stopVoiceRecognition is never called, then when the application is restarted
                // startVoiceRecognition always returns false whenever it is called.
                if (mBluetoothHeadset.startVoiceRecognition(mConnectedHeadset))
                {
                    log = "Profile listener startVoiceRecognition returns true"; //$NON-NLS-1$
                }
                else
                {
                    log = "Profile listener startVoiceRecognition returns false"; //$NON-NLS-1$
                }   
            }

            Log.d(TAG, log); 
        }

        // During the active life time of the app, a user may turn on and off the head set.
        // So register for broadcast of connection states.
        registerReceiver(mHeadsetBroadcastReceiver, 
                        new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));

        // Calling startVoiceRecognition does not result in immediate audio connection.
        // So register for broadcast of audio connection states. This broadcast will
        // only be sent if startVoiceRecognition returns true.
        IntentFilter f = new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
        f.setPriority(Integer.MAX_VALUE);
        registerReceiver(mHeadsetBroadcastReceiver, f);
    }
};


protected BroadcastReceiver mHeadsetBroadcastReceiver = new BroadcastReceiver()
{

    @Override
    public void onReceive(Context context, Intent intent)
    {           
        String action = intent.getAction();
        int state;
        int previousState = intent.getIntExtra(BluetoothHeadset.EXTRA_PREVIOUS_STATE, BluetoothHeadset.STATE_DISCONNECTED);
        String log = ""; 

        if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED))
        {
            state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
            if (state == BluetoothHeadset.STATE_CONNECTED)
            {
                mConnectedHeadset = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                // Audio should not be connected yet but just to make sure.
                if (mBluetoothHeadset.isAudioConnected(mConnectedHeadset))
                {
                    log = "Headset connected audio already connected";
                }
                else
                {

                    // Calling startVoiceRecognition always returns false here, 
                    // that why a count down timer is implemented to call
                    // startVoiceRecognition in the onTick and onFinish.
                    if (mBluetoothHeadset.startVoiceRecognition(mConnectedHeadset))
                    {
                        log = "Headset connected startVoiceRecognition returns true"; $NON-NLS-1$
                    }
                    else
                    {
                        log = "Headset connected startVoiceRecognition returns false";
                    }
                }
            }
            else if (state == BluetoothHeadset.STATE_DISCONNECTED)
            {
                // Calling stopVoiceRecognition always returns false here
                // as it should since the headset is no longer connected.
                mConnectedHeadset = null;
            }
        }
        else // audio
        {
            state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);

            mBluetoothHeadset.stopVoiceRecognition(mConnectedHeadset);

            if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED)
            {
                log = "Head set audio connected, cancel countdown timer";  
            }
            else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED)
            {
                // The headset audio is disconnected, but calling
                // stopVoiceRecognition always returns true here.
                boolean returnValue = mBluetoothHeadset.stopVoiceRecognition(mConnectedHeadset);
                log = "Audio disconnected stopVoiceRecognition return " + returnValue; 
            }
        }   

        log += "\nAction = " + action + "\nState = " + state 
                + " previous state = " + previousState; 
        Log.d(TAG, log);

    }
};

Like I said, I can detect the events, but I can't aboard the broadcast.

PX Developer
  • 8,065
  • 7
  • 42
  • 66
  • I'm trying to do something similar, so please keep us posted. Would you be able to share your code? As I understand, Android ICS intercepts long clicks on the media button and only known way to avoid it is to change the key map. That requires rooting. – SoloPilot Dec 19 '13 at 20:06
  • I actually set aside this for a while since I have to develop modules with a higher priority. When I go back to it I'll let you know if I find something. I'm gonna edit the post to add the code I used to detect the event. – PX Developer Dec 20 '13 at 08:42
4

For the Voice Dialer action, add the following to your manifest:

<action android:name="android.intent.action.VOICE_COMMAND" />
<category android:name="android.intent.category.DEFAULT" />

As you deduced from your debug logs, ACTION_VOICE_COMMAND is the triggered action, but without CATEGORY_DEFAULT, your app will not be considered. (I tested this with my own bluetooth headset, and it worked for me!)

Alice Purcell
  • 12,622
  • 6
  • 51
  • 57
1

Since you have a method that receives broadcasts, can't you try to print what intent you receive when performing a long click on your headset, so you would know what event your phone receives in this case and handle it?

@Override
    public void onReceive(Context context, Intent intent) {

        Log.d("SA", "ON RECEIVE" + intent.getAction()); // Print the received event

        ...

        abortBroadcast();
    }
Basile Perrenoud
  • 4,039
  • 3
  • 29
  • 52
  • I don't print the action because the onReceive method is not executed (neither with long click in my wired headset nor single click in my wireless headset). It's like the filter is wrong or something. – PX Developer Dec 09 '13 at 16:38
  • What about the log? When you connect your phone to eclipse, you can see the log in real time. Be sure to display everything (not only the log from your activity). Press the button of you headset (both short and long). Can you see an event triggered in the log when you press the button? (it might be difficult to find since you will certainly have a lot of log comming from your phone). You can try posting the log here – Basile Perrenoud Dec 09 '13 at 23:01
  • Hey Toaster, I did as you said and I think now I'm getting closer to intercepting the events. I edited my post and added the log output of both cases to the end of it (look for the EDIT section). Basically, the long click is using ACTION_VOICE_SEARCH, while the wireless single click is using ACTION_VOICE_COMMAND. I tried to filter them but it's not working. – PX Developer Dec 10 '13 at 09:51
  • I found something here: http://www.basic4ppc.com/android/forum/threads/bleutooth-handsfree-control.15772/ It is not really clear, but my guess is that you maybe have to disable the voice commands so you app can see the event. From your log, I think that the field "cmp=com.google.android.googlequicksearchbox/com.google.android.voicesearch.handsfree.HandsFreeIntentActivity" means that the event is handled by another app than yours – Basile Perrenoud Dec 10 '13 at 14:36
  • I've been testing with the headset and I think the problem is that my phone detects it as a HandsFree device, not as a regular headset. If I just connect the headset I can hear the phone calls through it, but I can't hear the media audio (like the youtube app), I need a third party app like BTmono to be able to use it as a regular headset. – PX Developer Dec 10 '13 at 15:54