Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Receive callback on all Android media button events all the time (even when another app is playing audio)

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!

like image 980
Mihai Avatar asked Jul 10 '16 05:07

Mihai


People also ask

What is a media callback?

Your media session callbacks call methods in several APIs to control the player, manage the audio focus, and communicate with the media session and media browser service.

What is the media key on my phone?

Media buttons are hardware buttons found on Android devices and other peripheral devices, for example, the pause/play button on a Bluetooth headset. When a user presses a media button, Android generates a KeyEvent , which contains a key code that identifies the button.

What is media browser service?

Media browser services enable applications to browse media content provided by an application and ask the application to start playing it. They may also be used to control content that is already playing by way of a MediaSession .

What is media session in Android?

android.media.session.MediaSession. Allows interaction with media controllers, volume keys, media buttons, and transport controls. A MediaSession should be created when an app wants to publish media playback information or handle media keys.


2 Answers

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?

like image 73
mehd azizi Avatar answered Sep 21 '22 18:09

mehd azizi


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.

like image 43
jianhua chen Avatar answered Sep 21 '22 18:09

jianhua chen