Receive callback on all Android media button event

2020-06-03 06:58发布

问题:

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!

回答1:

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?



回答2:

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.