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.
The documentation says
On Android 5.0 and higher, Android automatically dispatches media button events to your active media session by calling onMediaButtonEvent(). By default this callback translates the KeyEvent into the appropriate media session callback method that matches the key code
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)...
package fishpowered.best.browser.services;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
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 fishpowered.best.browser.Browser;
import fishpowered.best.browser.R;
import fishpowered.best.browser.utilities.SpeechBank;
import fishpowered.best.browser.utilities.TextToSpeechPlayer;
import fishpowered.best.browser.utilities.UrlHelper;
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;
private BroadcastReceiver broadcastReceiver;
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_???");
}
}
}
}
I've been stuck on this for a while so any help would be greatly appreciated.
from Catching android media button events
No comments:
Post a Comment