Catching android media button events
Asked Answered
X

3

9

I have an app that launches a foreground service to play some media and I want to be able to control it with media buttons on smart watches/headphones and control it from a mediastyle notification etc.

I cannot get the media buttons to work consistently though. In the logs I can see they are often sent to other apps even though I started my MediaSession and playback last.

But I cannot get it to work despite having a media session where I setActive(true) and having the media callback defined?

Manifest:

<service
    android:name=".services.MediaControllerService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.FOREGROUND_SERVICE">
    <intent-filter android:priority="999">
        <action android:name="android.intent.action.MEDIA_BUTTON"/>
    </intent-filter>
</service>

Code (Note the packages, it was hard in android 10 finding the correct combination of packages that worked together and gave me MediaStyle)...

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.media.session.MediaButtonReceiver;

public class TextToSpeechMediaControllerService extends Service { 
    public static final String START_SERVICE_ACTION_INTENT = "serviceStart";
    private TextToSpeechPlayer player;
    private MediaMetadataCompat mediaMetaData;
    private MediaSessionCompat mediaSession;
    private TextToSpeechMediaControllerService.AudioFocusHelper mAudioFocusHelper;
    private AudioManager mAudioManager;
    private final String NOTIFICATION_CHANNEL_TEXT_TO_SPEECH_CONTROLS = "fp_tts_media_controls";
    public MediaControllerCompat.TransportControls transportControls;
    private String pageTitle;
    private String pageAddress;
    private String pageDomain;
    private SpeechBank currentSpeechBank;
    private MediaButtonReceiver mediaButtonReceiver;
    private AudioFocusRequest audioFocusRequest;
    private AudioAttributes playbackAttributes;
    private Handler handler;

    public TextToSpeechMediaControllerService() {
    }

    @Override
    public void onCreate() {
        Log.d("TTSMEDIAPLAYER", "---------- STARTING SERVICE ----------");
        player = new TextToSpeechPlayer(this);
        mAudioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
        mAudioFocusHelper = new TextToSpeechMediaControllerService.AudioFocusHelper();

        mediaSession = new MediaSessionCompat(this, "fpt2s");
        mediaSession.setCallback(callback);
        mediaSession.setActive(true);
        handler = new Handler(); // something to do with handling delayed focus https://developer.android.com/guide/topics/media-apps/audio-focus#audio-focus-change
        transportControls = mediaSession.getController().getTransportControls();
        NotificationChannel channel = null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            channel = new NotificationChannel(NOTIFICATION_CHANNEL_TEXT_TO_SPEECH_CONTROLS,
                    getString(R.string.media_controls_notification_channel_title),
                    NotificationManager.IMPORTANCE_HIGH);
            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
        }
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("TTSMEDIAPLAYER", "---------- onStartCOmmand ("+intent.getAction()+", startId) ----------");
        MediaButtonReceiver.handleIntent(mediaSession, intent); // Required to catch media button events and send them to mediasession callback
        if (intent !=null && intent.getExtras()!=null){
            int closeCommand = intent.getExtras().getInt("swipeToClose", 0);
            if(closeCommand==1){
                Log.d("TTSMEDIAPLAYER", "Close service intent received.");
                // Pre-lollipop media style close button. Unable to test
                stopSelf();
                return super.onStartCommand(intent, flags, startId);
            }
            //Request audio focus
            if (false ) { // mAudioFocusHelper.requestAudioFocus() == false not needed because we request focus onPlay
                Log.d("TTSMEDIAPLAYER", "Starting and requesting focus...");
                //Could not gain focus
                stopSelf();
            }else {
                Log.d("TTSMEDIAPLAYER", "Focued. Starting...");
                boolean isPlaying = player.isPlaying();
                String nodesAsJsonString = intent.getExtras().getString("nodesAsJsonString", "[]");
                if (nodesAsJsonString != null && !nodesAsJsonString.equals("[]")) {
                    isPlaying = true;
                    // New TTS playback has been requested
                    pageTitle = intent.getExtras().getString("pageTitle", "");
                    pageAddress = intent.getExtras().getString("pageAddress", "");
                    pageDomain = UrlHelper.getDomain(pageAddress, true, true, true);
                    final Locale languageToSpeak = UrlHelper.getLanguageFromAddress(pageAddress);

                    try {
                        currentSpeechBank = new SpeechBank(nodesAsJsonString, languageToSpeak);
                    } catch (JSONException e) {
                        stopSelf();
                        return super.onStartCommand(intent, flags, startId);
                    }
                    mediaMetaData = new MediaMetadataCompat.Builder()
                            // TODO i guessed at these, I think this might be used on things like bluetooth speakers that have a display
                            .putString(MediaMetadata.METADATA_KEY_ARTIST, pageAddress)
                            .putString(MediaMetadata.METADATA_KEY_TITLE, pageTitle)
                            .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, pageTitle)
                            .build();
                    mediaSession.setMetadata(mediaMetaData);
                }
                Log.d("TTSMEDIAPLAYER", "isPlaying: " + isPlaying);
                /*mediaSession.setFlags(
                        MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | // apparently no longer needed
                                //MediaSession.FLAG_HANDLES_QUEUE_COMMANDS | //
                                MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS 
                );*/

                if(intent.getAction()!=null && intent.getAction().equals(START_SERVICE_ACTION_INTENT)) {
                    Log.d("TTSMEDIAPLAYER", "START_SERVICE_ACTION_INTENT detected, triggering transportcontrols.play");
                    transportControls.play();
                }
            }
        }
        return super.onStartCommand(intent, flags, startId); //  START_NOT_STICKY;?
    }

    private void updateNotificationAndMediaButtons(boolean isPlaying) {
        // ... notification stuff ...
        startForeground(1, notificationBuilder.build());
    }

    @Override
    public void onDestroy() {
        mediaSession.release();
        mAudioFocusHelper.abandonAudioFocus();
        player.freeUpResources();
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {

        @Override
        public void onSkipToNext() {
            Log.d("TTSMEDIAPLAYER", "SKIP TO NEXT");
            super.onSkipToNext();
            handleFastForward();
        }

        @Override
        public void onPlay() {
            Log.d("TTSMEDIAPLAYER", "onPLAY!");
            if(mAudioFocusHelper.requestAudioFocus()) {
                if(player.isPaused()){
                    Log.d("TTSMEDIAPLAYER", "(resuming)");
                    player.resume();
                }else{
                    Log.d("TTSMEDIAPLAYER", "(not started? playNew)");
                    player.playNew(currentSpeechBank);
                }
                // TODO TTS textToSpeechPlayer.play();
                PlaybackStateCompat state = new PlaybackStateCompat.Builder()
                        // Supported actions in current state
                        .setActions(
                                PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_STOP
                                        | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
                        )
                        // Current state
                        .setState(PlaybackStateCompat.STATE_PLAYING, player.getCurrentPosition(), 1, SystemClock.elapsedRealtime())
                        .build();
                mediaSession.setPlaybackState(state);
                updateNotificationAndMediaButtons(true);
            }
            super.onPlay();
        }

        @Override
        public void onPause() {
            Log.d("TTSMEDIAPLAYER", "onPAUSE!");
            player.pause();
            PlaybackStateCompat state = new PlaybackStateCompat.Builder()
                    // Set supported actions in current state
                    .setActions(
                            PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_STOP |
                                    PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND)
                    // Set current state
                    .setState(PlaybackStateCompat.STATE_PAUSED, player.getCurrentPosition(), 1, SystemClock.elapsedRealtime())
                    .build();
            mediaSession.setPlaybackState(state);
            updateNotificationAndMediaButtons(false);
            super.onPause();
        }

        @Override
        public void onSkipToPrevious() {
            Log.d("TTSMEDIAPLAYER", "SKIP TRACK PREV!");
            player.rewind();
            super.onSkipToPrevious();
        }

        @Override
        public void onFastForward() {
            Log.d("TTSMEDIAPLAYER", "FAST FORWARD!");
            super.onFastForward();
            handleFastForward();
        }

        @Override
        public void onRewind() {
            Log.d("TTSMEDIAPLAYER", "REWIND!");
            player.rewind();
            PlaybackStateCompat state = new PlaybackStateCompat.Builder()
                    // Set supported actions in current state
                    .setActions(
                            PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_STOP |
                                    PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND)
                    // Set current state
                    .setState(PlaybackStateCompat.STATE_PLAYING, player.getCurrentPosition(), 1, SystemClock.elapsedRealtime())
                    .build();
            mediaSession.setPlaybackState(state);
            updateNotificationAndMediaButtons(true);
            super.onRewind();
        }

        @Override
        public void onStop() {
            Log.d("TTSMEDIAPLAYER", "STOP!");
            player.stop();
            PlaybackStateCompat state = new PlaybackStateCompat.Builder()
                    // Set supported actions in current state
                    //    .setActions(null)
                    // Set current state
                    .setState(PlaybackStateCompat.STATE_STOPPED, player.getCurrentPosition(), 1, SystemClock.elapsedRealtime())
                    .build();
            mediaSession.setPlaybackState(state);
            mAudioFocusHelper.abandonAudioFocus();
            stopSelf();
            //super.onStop();
        }
    };

    private void handleFastForward() {
        boolean hasReachedEnd = player.fastForward();
        if(hasReachedEnd){
            transportControls.stop();
        }else{
            PlaybackStateCompat state = new PlaybackStateCompat.Builder()
                    // Set supported actions in current state
                    .setActions(
                            PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP |
                                    PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND)
                    // Set current state
                    .setState(PlaybackStateCompat.STATE_PLAYING, player.getCurrentPosition(), 1, SystemClock.elapsedRealtime())
                    .build();
            mediaSession.setPlaybackState(state);
            updateNotificationAndMediaButtons(true);
        }
    }

    /**
     * Helper class for managing audio focus related tasks.
     */
    private final class AudioFocusHelper
            implements AudioManager.OnAudioFocusChangeListener {

        private boolean mPlayOnAudioFocus = false;

        private boolean requestAudioFocus() {
            Log.d("TTSMEDIAPLAYER", "requestAudioFocus()...");
            playbackAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                    .build();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                        .setAudioAttributes(playbackAttributes)
                        .setAcceptsDelayedFocusGain(false)
                        .setOnAudioFocusChangeListener(mAudioFocusHelper, handler)
                        .build();
                int res = mAudioManager.requestAudioFocus(audioFocusRequest);
                if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
                    Log.d("TTSMEDIAPLAYER", "audio focus failed...");
                    return false;
                } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                    Log.d("TTSMEDIAPLAYER", "audio focus granted...");
                    return true;
                } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
                    Log.d("TTSMEDIAPLAYER", "audio focus DELAYED...");
                    // use case for this is imagine being in a phone call that has focus,
                    // then the user opens a game. The game
                    // should start playing audio once the call finishes.
                    return false; // todo?
                }
            }else{
                final int result = mAudioManager.requestAudioFocus(this,
                        AudioManager.STREAM_MUSIC,
                        AudioManager.AUDIOFOCUS_GAIN);
                return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
            }
            Log.d("TTSMEDIAPLAYER", "audio focus returning default!?");
            return false;
        }

        private void abandonAudioFocus() {
            Log.d("TTSMEDIAPLAYER", "abandonAudioFocus()");
            mAudioManager.abandonAudioFocus(this);
        }

        @Override
        public void onAudioFocusChange(int focusChange) {
            Log.d("TTSMEDIAPLAYER", "Audio focus changed...");
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:

                    Log.d("TTSMEDIAPLAYER", "Audio focus gained!");
                    if (mPlayOnAudioFocus && player.isPaused()) {
                        player.resume();
                        //} else if (isPlaying()) {
                        //    setVolume(MEDIA_VOLUME_DEFAULT);
                    }
                    mPlayOnAudioFocus = false;
                    break;

                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    Log.d("TTSMEDIAPLAYER", "Something about ducks!?");
                    // this might be for dropping the sound while something else happens (text notifications)
                    //setVolume(MEDIA_VOLUME_DUCK);
                    break;

                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    Log.d("TTSMEDIAPLAYER", "AUDIOFOCUS_LOSS_TRANSIENT!");
                    if (player.isPlaying()) {
                        // I think this is for temporary loss of focus e.g. calls/notificaitons
                        mPlayOnAudioFocus = true;
                        player.pause();
                    }
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    // Seems to be triggered when you press play in another media app (i.e. they requested focus)
                    Log.d("TTSMEDIAPLAYER", "AUDIOFOCUS_LOSS! abandoning focus, pausing speech");
                    mAudioManager.abandonAudioFocus(this);
                    if (player.isPlaying()) {
                        player.pause();
                        mPlayOnAudioFocus = false;
                    }
                    updateNotificationAndMediaButtons(false);
                    break;
                default:
                    Log.d("TTSMEDIAPLAYER", "AUDIOFOCUS_???");
            }
        }
    }
}

build.gradle

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.media:media:1.1.0'
...

I've been stuck on this for a while so any help would be greatly appreciated.

Logs:

? I/MusicController: [MediaSessionMonitor.java:153:onActiveSessionsChanged()] oooooo 
? I/MusicController: [MediaSessionMonitor.java:302:clear()] oooooo 
? D/MusicController: [MediaSessionMonitor.java:99:clearMediaContorllersMap()] oooooo Controller is already empty
? W/MusicController: [MediaSessionMonitor.java:171:updateMediaControllers()] oooooo Controller is empty
? I/MediaFocusControl:  AudioFocus  requestAudioFocus() from uid/pid 10345/8858 clientId=android.media.AudioManagerEx@63b6ee9fishpowered.bar.services.TextToSpeechMediaControllerService$AudioFocusHelper@44f676e req=1 flags=0x0
? I/MusicController: [MediaSessionMonitor.java:153:onActiveSessionsChanged()] oooooo 
? I/MusicController: [MediaSessionMonitor.java:174:updateMediaControllers()] oooooo List size :1
? I/MusicController: [MediaSessionMonitor.java:178:updateMediaControllers()] oooooo MediaController received packageName foo.bar
? D/MusicController: [MediaSessionMonitor.java:74:addToMediaContorllersMap()] oooooo Added = android.media.session.MediaSession$Token@986e7dd
? I/MusicController: [MediaSessionMonitor.java:78:addToMediaContorllersMap()] oooooo mMediaContorllersMap.size() = 1
? I/MusicController: [MediaSessionMonitor.java:222:checkAndUpdateMusicController()] oooooo is MusicController Updated = false
? I/MusicController: [MediaSessionMonitor.java:182:updateMediaControllers()] oooooo mMediaContorllersMap size = 1
? I/MediaFocusControl:  AudioFocus  requestAudioFocus() from uid/pid 10345/8858 clientId=android.media.AudioManagerEx@63b6ee9fishpowered.bar.services.TextToSpeechMediaControllerService$AudioFocusHelper@44f676e req=1 flags=0x0
? I/MusicController: [MediaControllerCallbackWrapper.java:55:onMetadataChanged()] oooooo This callback is from foo.bar
? I/MusicController: [MediaControllerCallbackWrapper.java:47:onPlaybackStateChanged()] oooooo This callback is from foo.bar
? D/MusicController: [MediaSessionMonitor.java:123:handleMessage()] oooooo MediaSessionMonitor.MsgHandler msg = 2
? D/MusicController: [MediaSessionMonitor.java:205:checkAndUpdateMusicController()] oooooo state = 3, action = 585
? I/MusicController: [MediaSessionMonitor.java:234:isValidStateToRegister()] oooooo isValidStateToRegister = true
? I/MusicController: [MediaSessionMonitor.java:257:isValidMetadataToRegister()] oooooo isValidMetadataToRegister = true
? I/MusicController: [MediaSessionMonitor.java:245:isValidActionToRegister()] oooooo isValidActionToRegister = false
? I/MusicController: [MediaSessionMonitor.java:222:checkAndUpdateMusicController()] oooooo is MusicController Updated = false
? D/MusicController: [MediaSessionMonitor.java:123:handleMessage()] oooooo MediaSessionMonitor.MsgHandler msg = 1
? D/MusicController: [MediaSessionMonitor.java:205:checkAndUpdateMusicController()] oooooo state = 3, action = 585
? I/MusicController: [MediaSessionMonitor.java:234:isValidStateToRegister()] oooooo isValidStateToRegister = true
? I/MusicController: [MediaSessionMonitor.java:257:isValidMetadataToRegister()] oooooo isValidMetadataToRegister = true
? I/MusicController: [MediaSessionMonitor.java:245:isValidActionToRegister()] oooooo isValidActionToRegister = false
? I/MusicController: [MediaSessionMonitor.java:222:checkAndUpdateMusicController()] oooooo is MusicController Updated = false
? I/MusicController: [MediaControllerCallbackWrapper.java:47:onPlaybackStateChanged()] oooooo This callback is from foo.bar
? D/MusicController: [MediaSessionMonitor.java:123:handleMessage()] oooooo MediaSessionMonitor.MsgHandler msg = 1
? D/MusicController: [MediaSessionMonitor.java:205:checkAndUpdateMusicController()] oooooo state = 3, action = 585
? I/MusicController: [MediaSessionMonitor.java:234:isValidStateToRegister()] oooooo isValidStateToRegister = true
? I/MusicController: [MediaSessionMonitor.java:257:isValidMetadataToRegister()] oooooo isValidMetadataToRegister = true
? I/MusicController: [MediaSessionMonitor.java:245:isValidActionToRegister()] oooooo isValidActionToRegister = false
? I/MusicController: [MediaSessionMonitor.java:222:checkAndUpdateMusicController()] oooooo is MusicController Updated = false
? V/MediaRouter: Adding route: RouteInfo{ name=Phone, description=null, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO , presentationDisplay=null }
? V/MediaRouter: Adding route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }
? V/MediaRouter: Selecting route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }
? V/MediaRouter: Audio routes updated: AudioRoutesInfo{ type=SPEAKER, bluetoothName=DummyDevice }, a2dp=true
? W/MediaSessionCompat: Couldn't find a unique registered media button receiver in the given context.
? I/MusicController: [MediaSessionMonitor.java:153:onActiveSessionsChanged()] oooooo 
? I/MusicController: [MediaSessionMonitor.java:174:updateMediaControllers()] oooooo List size :1
? I/MusicController: [MediaSessionMonitor.java:178:updateMediaControllers()] oooooo MediaController received packageName foo.bar
? D/MusicController: [MediaSessionMonitor.java:76:addToMediaContorllersMap()] oooooo Already exist = android.media.session.MediaSession$Token@986e7dd
? I/MusicController: [MediaSessionMonitor.java:78:addToMediaContorllersMap()] oooooo mMediaContorllersMap.size() = 1
? D/MusicController: [MediaSessionMonitor.java:205:checkAndUpdateMusicController()] oooooo state = 3, action = 585
? I/MusicController: [MediaSessionMonitor.java:234:isValidStateToRegister()] oooooo isValidStateToRegister = true
? I/MusicController: [MediaSessionMonitor.java:257:isValidMetadataToRegister()] oooooo isValidMetadataToRegister = true
? I/MusicController: [MediaSessionMonitor.java:245:isValidActionToRegister()] oooooo isValidActionToRegister = false
? I/MusicController: [MediaSessionMonitor.java:222:checkAndUpdateMusicController()] oooooo is MusicController Updated = false
? I/MusicController: [MediaSessionMonitor.java:182:updateMediaControllers()] oooooo mMediaContorllersMap size = 1
? V/MediaRouter: Selecting route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }
? V/MediaRouter: Adding route: RouteInfo{ name=Phone, description=null, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO , presentationDisplay=null }
? V/MediaRouter: Adding route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }
? V/MediaRouter: Selecting route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }
? V/MediaRouter: Audio routes updated: AudioRoutesInfo{ type=SPEAKER, bluetoothName=DummyDevice }, a2dp=true
? V/MediaRouter: Selecting route: RouteInfo{ name=DummyDevice, description=Bluetooth audio, status=null, category=RouteCategory{ name=System types=ROUTE_TYPE_LIVE_AUDIO ROUTE_TYPE_LIVE_VIDEO  groupable=false }, supportedTypes=ROUTE_TYPE_LIVE_AUDIO , presentationDisplay=null }

Also I'm seeing an audio permission error but I'm not sure if this is related:

W/MediaListnrAuthObsrvr: registration failed - not an approved notification listener yet?
    java.lang.SecurityException: Missing permission to control media.
        at android.os.Parcel.readException(Parcel.java:1951)
        at android.os.Parcel.readException(Parcel.java:1897)
        at android.media.session.ISessionManager$Stub$Proxy.addSessionsListener(ISessionManager.java:342)
        at android.media.session.MediaSessionManager.addOnActiveSessionsChangedListener(MediaSessionManager.java:226)
        at android.media.session.MediaSessionManager.addOnActiveSessionsChangedListener(MediaSessionManager.java:189)
        at com.google.android.clockwork.common.media.DefaultMediaSessionManagerWrapper.addOnActiveSessionsChangedListener(AW771527612:8)
        at com.google.android.clockwork.companion.mediacontrols.api21.MediaSessionListenerAuthorizationObserver.register(AW771527612:12)
        at com.google.android.clockwork.companion.mediacontrols.api21.MediaSessionListenerAuthorizationObserver.<init>(AW771527612:5)
        at com.google.android.clockwork.companion.mediacontrols.api21.MediaRemoteControllerApi21.start(AW771527612:13)
Xerarch answered 20/10, 2019 at 10:11 Comment(2)
Tips? Suggestions? Anything? This is pretty much all the code except the MediaStyle notification, am I supposed to do anything else? Are the packages okay? What else could cause other apps to steal the KeyEvents? etcXerarch
Check my answer. I think it should solve the problemKrebs
X
6

I eventually discovered why this wasn't working for me. It seems like there is a bug with Android Oreo that media focus cannot be properly obtained despite requesting for focus and playing audio using the TTS engine.

The workaround was to play a silent wav file from the system mediaplayer before commencing the TTS

Xerarch answered 5/2, 2020 at 14:8 Comment(4)
Did you find any sources that support your bug claim? I'm having the same issue as you, however I don't think this is an SDK bug since there are apps that correctly handle this flow. I also thought of playing a blank audio file onResume so it catches the media buttons focus but it's definitely a hack which suggests that we are not doing something right.Globigerina
I didn't find any official bug report about it, just another developer referencing the bug and stating it can be fixed by playing a silent wav directly before trying to play your TTS which instantly fixed all of my problems. Also my code would work if I didn't use TTS but used the built in mediaplayer, so if there's a better fix I'm all ears but at least the silent wav hack worked for meXerarch
Thanks a lot ! I am on a similar problem for a long time and this was the solution ! 👍Rieth
Yep the issue has been reported to Google here : issuetracker.google.com/issues/249741615Rieth
K
3

As explained here you must call setActions() on your PlaybackStateCompat.Builder to tell exactly what actions you support - if you don't set that then you won't get any of the callbacks relating to media buttons. You are setting these actions in the callback which is why it's not working.

....

private Handler handler;
private PlaybackStateCompat.Builder mStateBuilder;
.....

Add the following in onCreate.

....
mediaSession = new MediaSessionCompat(this, "fpt2s");

mStateBuilder = new PlaybackStateCompat.Builder()
            .setActions(
                    PlaybackStateCompat.ACTION_PLAY |
                            PlaybackStateCompat.ACTION_PAUSE |
                            PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
                          PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
                            PlaybackStateCompat.ACTION_PLAY_PAUSE);

mediaSession.setPlaybackState(mStateBuilder.build());
mediaSession.setCallback(callback);
mediaSession.setActive(true);

.......
Krebs answered 1/11, 2019 at 15:1 Comment(4)
Sorry but I tried this and it didn't work, my media buttons worked initially but then I tried starting another media app on my phone and then playing media from my app again. Once the other media apps have been opened (even if I close them) they tend to steal the keyevents e.g. if I'm playing my media, then hit my pause button on my watch, it pauses it but only because it has sent a play to the other app and I lose audio focus. I noticed this in the logs: [MediaSessionMonitor.java:245:isValidActionToRegister()] oooooo isValidActionToRegister = falseXerarch
It does seem you have the right idea that the actions aren't always registering and therefore it doesn't allow the callback to be triggered. Unfortunately my girlfriend really wants me to help with cleaning so I'll have to experiment more with this later. I'll update my original post with some of the logs I see.Xerarch
I tried what you mentioned in your previous comment. Its working fine for me. Cleaning the code? It seems you are not in a hurry after all and it does not need the attention you tried to get with the bounty. I hope you are careful with this next time :)Krebs
I've been stuck on this for weeks and tried a million different things so it's very odd you should question my desire to put a bounty on it. I also tried your answer and commented before the bounty expired so what exactly are you sore about? I copy and pasted your code and tried a ton of variations on it, it still didn't work. I also watched the vid and spent a further few hours looking into this and got nowhere. Something funky is going on. isValidActionToRegister is always showing false for example so thanks for the help but unfortunately it didn't solve the underlying problemXerarch
T
2

Sorry for the late reply, but maybe it will help someone. If you are using MediaSessionConnector with MediaSessionCompat here is the code:

    mediaSessionConnector.setMediaButtonEventHandler(new MediaSessionConnector.MediaButtonEventHandler() {
        @NonNullApi
        @Override
        public boolean onMediaButtonEvent(Player player, Intent mediaButtonEvent) {
            KeyEvent dugme;
            if (Build.VERSION.SDK_INT >= 33) {
                dugme = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent.class);
            }else{
                dugme = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            }
            toast("command: "+dugme);
            return false;
        }
    });

It should be remembered that you have to catch the button press with the KeyCode KEYCODE_HEADSETHOOK from the headphones if you want to play or stop media.

If you only use only MediaSessionCompat here is the code:

    mediaSession.setCallback(new MediaSessionCompat.Callback() {
        @Override
        public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
            KeyEvent dugme;
            if (Build.VERSION.SDK_INT >= 33) {
                dugme = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent.class);
            }else{
                dugme = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            }
            toast("command: "+dugme);
            return super.onMediaButtonEvent(mediaButtonEvent);
        }
    });

The same goes for capturing button presses from the headset. Happy coding.

Tilda answered 18/9, 2022 at 20:28 Comment(1)
Note that the OP was specifically interested in this for TTS, not for ordinary audio playback. Alas, it appears that there is a bug/feature that prevents that. I say "feature" because it is marked as "wontfix" by Google: issuetracker.google.com/issues/249741615Albania

© 2022 - 2024 — McMap. All rights reserved.