9

Background Info: I need to detect whenever a user presses the play/pause button found on most headsets (KEYCODE_MEDIA_PLAY_PAUSE).

I have it all mostly working using MediaSessions, but when another app starts playing audio, I stop getting callbacks.

It seems like this is because the app that's playing audio created its own MediaSession and Android sends KeyEvents only to the newest MediaSession. To prevent this I create an OnActiveSessionsChangedListener and create a new MediaSession every time it fires.

This does work, but every time I create a new MediaSession, the listener fires again, so I find myself stuck in an inf loop.

My Question: does anyone know how I can do any of the following??:

  • Prevent other apps from stealing my media button focus
  • Detect when I've lost media button focus to another app, so I can create a new MediaSession only then, rather then whenever the active sessions change
  • Check if I currently already have media button focus so I needlessly create a new MediaSession

What didn't work:

  • BroadcastReceiver on AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION didn't work because apps have to manually trigger that Broadcast, and many apps, like NPR One do not
  • AudioManager.OnAudioFocusChangeListener didn't work because it requires I have audio focus
  • BroadcastReceiver with max priority on android.intent.action.MEDIA_BUTTON & calling abortBroadcast(), but when other apps were playing audio, my receiver wasn't triggered. Also, other apps can set max priority as well.

My Code:

mMediaSessionManager.addOnActiveSessionsChangedListener(controllers -> {
    boolean updateButtonReceiver = false;

    // recreate MediaSession if another app handles media buttons
    for (MediaController mediaController : controllers) {
        if (!TextUtils.equals(getPackageName(), mediaController.getPackageName())) {
            if ((mediaController.getFlags() & (MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)) != 0L) {
                updateButtonReceiver = true;
            }
        }

    }

    if (updateButtonReceiver) {
        // using a handler with a delay of about 2 seconds because this listener fires very often.
        mAudioFocusHandler.removeCallbacksAndMessages(null);
        mAudioFocusHandler.sendEmptyMessageDelayed(0, AUDIO_FOCUS_DELAY_MS);
    }
}, ClickAppNotificationListener.getComponentName(this));

Here is the handler that gets triggered:

private final Handler mAudioFocusHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (mShouldBeEnabled) {
            updateButtonReceiverEnabled(true);
        }
    }
};

And finally here is the method that the Handler triggers:

private void updateButtonReceiverEnabled(boolean shouldBeEnabled) {
    // clear old session (not sure if this is necessary)
    if (mMediaSession != null) {
        mMediaSession.setActive(false);
        mMediaSession.setFlags(0);
        mMediaSession.setCallback(null);
        mMediaSession.release();
        mMediaSession = null;
    }

    mMediaSession = new MediaSessionCompat(this, MEDIA_SESSION_TAG);
    mMediaSession.setCallback(mMediaButtonCallback);
    mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    mMediaSession.setPlaybackToLocal(AudioManager.STREAM_MUSIC);
    mMediaSession.setActive(true);
    mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
            .setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
            .setState(PlaybackStateCompat.STATE_CONNECTING, 0, 0f)
            .build());

    if (shouldBeEnabled != mShouldBeEnabled) {            
        getPackageManager().setComponentEnabledSetting(mMediaButtonComponent,
                shouldBeEnabled
                        ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }

    mShouldBeEnabled = shouldBeEnabled;
}

Thanks!

Mihai
  • 296
  • 2
  • 12
  • Have you found any solution? I am facing the same issue. – Krutik Feb 08 '17 at 13:44
  • Nothing better than before: registering a MediaSessionManager.OnActiveSessionsChangedListener and creating a new MediaSession every time it fires IF something in the list of MediaControllers has changed (an app was added to the list or one changed states: MediaController.getPlaybackState().getStateInt). jianhua suggested adding another check: only create a new MediaSession if our app is not the first entry in the list of MediaControllers, haven't added that bit yet, but if it works as advertised, it seems like a good optimization. – Mihai Feb 09 '17 at 18:20
  • @Mihai did you ever get this working? Wondering if Jianhua's idea worked for you. – DiscDev Jan 16 '21 at 04:16

2 Answers2

2

if you just want to capture MediaButton you can register a BroadcastReceiver to get Media Button action all the time .

MediaButtonIntentReceiver class :

public class MediaButtonIntentReceiver extends BroadcastReceiver {

  public MediaButtonIntentReceiver() {
    super();
    }

 @Override
 public void onReceive(Context context, Intent intent) {
     String intentAction = intent.getAction();
     if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
        return;
       }
     KeyEvent event =   (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
     if (event == null) {
        return;
       }
     int action = event.getAction();
     if (action == KeyEvent.ACTION_DOWN) {
          // do something
       Toast.makeText(context, "BUTTON PRESSED!", Toast.LENGTH_SHORT).show(); 
        }
    abortBroadcast();
  }
}

add this to manifest.xml:

<receiver android:name=".MediaButtonIntentReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

and register your BroadcastReceiver like this ( in main activity)

IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
MediaButtonIntentReceiver r = new MediaButtonIntentReceiver();
filter.setPriority(1000); 
registerReceiver(r, filter); 

also look at :

How to capture key events from bluetooth headset with android

How do I intercept button presses on the headset in Android?

Community
  • 1
  • 1
mehd azizi
  • 594
  • 1
  • 5
  • 16
  • 2
    I tried this before, sry I forgot to add it to the list of things I tried originally. The result was: if another app is playing audio they will get the media button presses instead of me. Also, there is no way for me to guarantee other apps aren't also setting max priority as well. – Mihai Jul 11 '16 at 16:40
1

The controllers you get in OnActiveSessionsChangedListener is ordered by priority. You only have to create a new MediaSession if you see that your MediaSessionis not the first one in the list.

Note that you might still run into an infinite loop if there is another app contending the media key events using the same approach.