I've tried everything and can't get Bluetooth controls to respond in my media app. I've read over the documentation a hundred times and I still can't get it to work. My Bluetooth headphones respond to other media apps on my device so it's something in my app.
According to the documentation, apps running on Android 8 and above should automatically respond to Bluetooth controls if the app uses a MediaBrowserService. This is what I've written:
MediaBrowserService:
public class MediaPlayerService extends MediaBrowserServiceCompat {
private static MediaPlayer mMediaPlayer;
private MediaSessionCompat mMediaSessionCompat;
private Context mContext;
private PlaybackStateCompat.Builder mPlaybackBuilder;
public MediaPlayerService() {
super();
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
mMediaSessionCompat = new MediaSessionCompat(this, MediaPlayerService.class.getSimpleName());
mMediaSessionCompat.setCallback(mMediaSessionCallback);
setSessionToken(mMediaSessionCompat.getSessionToken());
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
mMediaPlayer.setAudioAttributes(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build());
mPlaybackBuilder = new PlaybackStateCompat.Builder();
mPlaybackBuilder.setActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE
);
}
public int onStartCommand(Intent intent, int flags, int startId) {
MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
return super.onStartCommand(intent, flags, startId);
}
private final MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
super.onPlay();
setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING, 0);
mMediaPlayer.start();
}
@Override
public void onPause() {
super.onPause();
setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED, 0);
mMediaPlayer.pause();
}
@Override
public void onPlayFromUri(final Uri uri, final Bundle extras) {
super.onPlayFromUri(uri, extras);
try {
mMediaSessionCompat.setActive(true);
mMediaPlayer.setDataSource(uri.toString());
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(mp -> {
mMediaPlayer.start();
setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING, 0);
final String channelID = getPackageName().concat(".playback");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
final NotificationChannel channel = new NotificationChannel(channelID, "Test", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setLocalOnly(true)
.setContentTitle("Test")
.setOngoing(true);
builder.addAction(new NotificationCompat.Action(
android.R.drawable.ic_media_pause, "Pause",
MediaButtonReceiver.buildMediaButtonPendingIntent(mContext,
PlaybackStateCompat.ACTION_PLAY_PAUSE)));
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mMediaSessionCompat.getSessionToken()));
final NotificationManagerCompat manager = NotificationManagerCompat.from(getApplicationContext());
manager.notify(1, builder.build());
mMediaSessionCompat.setPlaybackState(mPlaybackBuilder.build());
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void setMediaPlaybackState(final int state, final int position) {
mPlaybackBuilder.setState(state, position, 1.0f);
mMediaSessionCompat.setPlaybackState(mPlaybackBuilder.build());
}
};
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, final int clientUid, @Nullable Bundle rootHints) {
if (clientPackageName.equalsIgnoreCase(getPackageName())) {
final Bundle extras = new Bundle();
extras.putBoolean("android.media.browse.CONTENT_STYLE_SUPPORTED", true);
extras.putInt("android.media.browse.CONTENT_STYLE_PLAYABLE_HINT", 1);
extras.putInt("android.media.browse.CONTENT_STYLE_BROWSABLE_HINT", 2);
return new BrowserRoot(getString(R.string.app_name), extras);
}
return null;
}
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> result) {
}
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bug.audiofocusbug">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service
android:name=".MediaPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>
UPDATE:
For a test, I added a receiver
to my AndroidManifest:
<receiver android:name=".MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
public class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String intentAction = intent.getAction();
Toast.makeText(context, "debug media button test", Toast.LENGTH_SHORT).show();
}
}
and the receiver
is hit when pressing the action in the notification but it's not hit when pressing the Bluetooth controls
UPDATE 2
I'm seeing entries like this in the Logcat:
Sending KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_MEDIA_PLAY, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=0, downTime=0, deviceId=-1, source=0x0, displayId=0 } to com.bug.audiofocusbug/MediaPlayerService (userId=0)
Sending KeyEvent { action=ACTION_UP, keyCode=KEYCODE_MEDIA_PAUSE, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=0, downTime=0, deviceId=-1, source=0x0, displayId=0 } to com.bug.audiofocusbug/MediaPlayerService (userId=0)
It looks like Android is sending the correct command but my app is not recognizing it for some reason.
Hopefully this helps someone in the future but the issue was I need to added both
PlaybackStateCompat.ACTION_PLAY
andPlaybackStateCompat.ACTION_PAUSE
to thePlaybackStateCompat.Builder
object like this: