Cannot get app to respond to Bluetooth controls

159 Views Asked by At

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.

1

There are 1 best solutions below

1
On

Hopefully this helps someone in the future but the issue was I need to added both PlaybackStateCompat.ACTION_PLAY and PlaybackStateCompat.ACTION_PAUSE to the PlaybackStateCompat.Builder object like this:

 mPlaybackBuilder.setActions(
            PlaybackStateCompat.ACTION_PLAY_PAUSE |
                    PlaybackStateCompat.ACTION_PLAY |
                    PlaybackStateCompat.ACTION_PAUSE
    );