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:
MediaSessionCompat-VolumeProviderCompatnot firing at allMediaSessionCompat.Callback, methodonMediaButtonEventis getting called only for on/off/call button. Besides that method isn't called at all when IsetBluetoothScoOn(true)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)- trying to read from
BluetoothSocket,socket.isconnected()istruebut hangs on nextsocket.getInputStream().read(buffer)the line waiting for data, never getting a single byte, timeout or exception... - 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...)