Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle bluetooth headset clicks (ACTION_VOICE_COMMAND and ACTION_WEB_SEARCH) on Android

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.

like image 620
PX Developer Avatar asked Dec 05 '13 11:12

PX Developer


People also ask

Why are my Bluetooth headphones making a clicking noise?

As previously mentioned, if your Bluetooth headphones are crackling, the problem might be the Bluetooth interference. You can try and turn off certain devices that could be causing interference, or move into a different area closer to the audio source.

How do I stop Bluetooth interference between headphones and mouse?

Turn other devices off and on again. A simple (but effective) way of isolating the interfering device. Turn a device off, then check for interference. If there's still interference, turn the device back on, and repeat with the next device. Continue this process until you find the device that was causing interference.

How can I use Bluetooth headset and mic at the same time Android?

Turn on Android bluetooth, connect to your bluetooth headphone. Start this app, try music 1 or 2 first, to make sure the sound audio goes to the bluetooth headset and not through internal speaker. Then start the Listen function. Voice from built-in mic of the Android phone will transmit to the bluetooth headphone.


2 Answers

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.

like image 190
PX Developer Avatar answered Sep 19 '22 15:09

PX Developer


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!)

like image 33
Alice Purcell Avatar answered Sep 20 '22 15:09

Alice Purcell