Handling Bluetooth headset keys

Viewed 483

I'm looking for a reliable way to handle volume button presses on Bluetooth headset. My testing device is Xiaomi Mi Headset, but this feature should work for all basic headsets.

Freshly paired headset volume buttons are changing volume, but only "internally"... Volume set in Android doesn't change, standstill. It seems like in this case volume is changed only on headphone - max value set on Android not necessary makes max value on earphone (e.g. 1/10 volume on device = 1/10 volume on headset at most). When I open my "main" app (which is already handling bt connection) and turn on SCO then I can change phone call volume with headset buttons, Android is showing controls on the screen. This second case is interesting to me a bit more, but any method will be sufficient for handling these keys...

I've created a new project and experimenting, what I've tried:

  1. MediaSessionCompat - VolumeProviderCompat not firing at all
  2. MediaSessionCompat.Callback, method onMediaButtonEvent is getting called only for on/off/call button. Besides that method isn't called at all when I setBluetoothScoOn(true)
  3. ACTION_VENDOR_SPECIFIC_HEADSET_EVENT - I'm getting only one or two AT commands when headset starts (battery info), not getting any data with button presses (both SCO on and off)
  4. trying to read from BluetoothSocket, socket.isconnected() is true but hangs on next socket.getInputStream().read(buffer) the line waiting for data, never getting a single byte, timeout or exception...
  5. obviously these clicks aren't KeyEvents

related snippets - for 1. and 2.:

private void setupMediaSession() {
    MediaSessionCompat mediaSession = new MediaSessionCompat(this, "PlayerService");
    mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
            .setState(PlaybackStateCompat.STATE_PLAYING, 0, 0) //you simulate a player which plays something.
            .build());

    VolumeProviderCompat volumeProvider = new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, /*max volume*/100, /*initial volume level*/50) {
        @Override
        public void onAdjustVolume(int direction) {
            Timber.i("TESTTEST onAdjustVolume direction: %s", direction);
        }
    };
    mediaSession.setPlaybackToRemote(volumeProvider);

    MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() {

        @Override
        public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
            Timber.i("TESTTEST onMediaButtonEvent mediaButtonEvent: %s", mediaButtonEvent.getAction());
            return true;
        }
    };
    mediaSession.setCallback(mMediaSessionCallback);
    AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC, 48000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
            AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM);
    at.play();
    mediaSession.setActive(true);
    Timber.i("TESTTEST setupMediaSession isActive: %s", mediaSession.isActive());
}

for 3.

private void setupVendorReceiver() {
    IntentFilter intentFilter = new IntentFilter(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
    /*intentFilter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY +
            '.'+ 911); // Xiaomi*/
    for(int i=0; i< 3000; i++) {
        intentFilter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY +
                '.' + i);
    }
    registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Timber.i("TESTTEST setupVendor onReceive: %s", intent.getAction());
            for (String key: intent.getExtras().keySet())
            {
                Timber.i( "TESTTEST, "+key + ":"+intent.getExtras().get(key));
                if(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS.equals(key)){
                    Object[] args = (Object[]) intent.getExtras().get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
                    for(Object arg : args){
                        Timber.i( "TESTTEST, arg: %s", arg);
                    }
                }
            }
        }
    }, intentFilter);
}

for 4.

private void socketConnection() {
    Set<BluetoothDevice> bs = BluetoothAdapter.getDefaultAdapter().getBondedDevices();
    for (BluetoothDevice device : bs) {
        Timber.i("TESTTEST device: %s", device);
        try {
            socket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
            break;
        } catch (IOException e) {
            Timber.e(e);
        }
    }
    new Thread() {
        @Override
        public void run() {
            InputStream in;
            OutputStream out;
            try {
                socket.connect();
                in = socket.getInputStream();
                out = socket.getOutputStream();
            } catch (IOException e) {
                Timber.e(e);
                socket = null;
                return;
            }

            while (isRunning) {
                try {
                    Timber.i("TESTTEST socket.isConnected() %s", socket.isConnected());
                    if (socket.isConnected()) {
                        int bytesRead = in.read(buffer);
                        Timber.i("TESTTEST bytesRead %s buffer %s", bytesRead, buffer);
                    }
                } catch (IOException e) {
                    Timber.e(e);
                }
            }
        }
    }.start();
}

for 5.

@Override
public boolean onKeyDown(final int keyCode, KeyEvent event) {
    Timber.i("TESTTEST onKeyDown: %s", keyCode);
    return super.onKeyDown(keyCode, event);
}

With above snippets turning on paired headset (ACL), then running my app gives this log

com.example.myapplication I/MainActivity: TESTTEST onCreate
com.example.myapplication I/BluetoothSoc: TESTTEST device: 1C:52:16:3E:B4:03
com.example.myapplication I/MainActivity: TESTTEST setupMediaSession isActive: true
com.example.myapplication I/BluetoothSoc: TESTTEST socket.isConnected() true

and nothing else never when pressing volume keys... any advise?

PS 1. I've found some doc for HD1-R5 headset, in which we have

PTT only worked for MobilitySound dongles, ICOM ,Kenwood radios.

PTT means volume keys or dedicated one (answering call on usual models). also found HD1-R6 doc, which in specs looks exacly the same with one and only exception: instead of above line it have

PTT only worked for Android cellphone with below POCZELLO,ESChat,WAVE,Kodiak,GroupTalk and others.

there is also YT video with working R6... what is the difference between these two devices?

PS 2. Xiaomi supports HFP/A2DP/HSP/AVRCP profiles (manual found here), these two above MobilitySound headsets supports HFP,HSP,A2DP

edit: answering PS 1. - difference is in AT commands send by headset volume keys. R5 is sending some "well-known" (e.g. TETRA doc):

AT+CTKST=0,1
AT+CTKST=0,0

and R6 is using commands noted in Android docs in some example HERE:

AT+XEVENT=TALK,1
AT+XEVENT=TALK,0

probably, didn't tested it, don't have any of these - AT commands took from PR5 and PR6, newer versions of R5 and R6. Still common Xiaomi model isn't sending anything in "paired-only mode" (ACL) - as described on top it changes "internal" volume - and with SCO turned on it seems like these AT commands are handled by some internal (native?) Bluetooth controller, passed strictly to volume control and further aren't passed to socket/apps

edit 2: can confirm that PR6 works and custom/third button is sending TALK AT cmd, can be handled using snippet #3. still this device does have volume buttons and like above - can't/don't know how intercept them... Also I've found more manufacturers producing almost same BT device/accessory in two variants - for Android and for others (and fun fact: iOS oftenly works with both...)

0 Answers
Related