public class

MediaButtonReceiver

extends BroadcastReceiver

 java.lang.Object

↳BroadcastReceiver

↳androidx.media.session.MediaButtonReceiver

Gradle dependencies

compile group: 'androidx.media', name: 'media', version: '1.7.0'

  • groupId: androidx.media
  • artifactId: media
  • version: 1.7.0

Artifact androidx.media:media:1.7.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.media:media com.android.support:support-media-compat

Androidx class mapping:

androidx.media.session.MediaButtonReceiver android.support.v4.media.session.MediaButtonReceiver

Overview

A media button receiver receives and helps translate hardware media playback buttons, such as those found on wired and wireless headsets, into the appropriate callbacks in your app.

You can add this MediaButtonReceiver to your app by adding it directly to your AndroidManifest.xml:

 <receiver android:name="androidx.media.session.MediaButtonReceiver" >
   <intent-filter>
     <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
 </receiver>
 
This class assumes you have a in your app that controls media playback via a android.support.v4.media.session.MediaSessionCompat. Once a key event is received by MediaButtonReceiver, this class tries to find a that can handle , and a MediaBrowserServiceCompat in turn. If an appropriate service is found, this class forwards the key event to the service. If neither is available or more than one valid service/media browser service is found, an java.lang.IllegalStateException will be thrown. Thus, your app should have one of the following services to get a key event properly.

Service Handling ACTION_MEDIA_BUTTON

A service can receive a key event by including an intent filter that handles :
 <service android:name="com.example.android.MediaPlaybackService" >
   <intent-filter>
     <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
 </service>
 
Events can then be handled in by calling MediaButtonReceiver.handleIntent(MediaSessionCompat, Intent), passing in your current android.support.v4.media.session.MediaSessionCompat:
 private MediaSessionCompat mMediaSessionCompat = ...;

 public int onStartCommand(Intent intent, int flags, int startId) {
   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
   return super.onStartCommand(intent, flags, startId);
 }
 
This ensures that the correct callbacks to will be triggered based on the incoming .

Note: Once the service is started, it must start to run in the foreground.

MediaBrowserService

If you already have a MediaBrowserServiceCompat in your app, MediaButtonReceiver will deliver the received key events to the MediaBrowserServiceCompat by default. You can handle them in your .

Summary

Constructors
publicMediaButtonReceiver()

Methods
public static PendingIntentbuildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, long action)

Creates a broadcast pending intent that will send a media button event.

public static PendingIntentbuildMediaButtonPendingIntent(Context context, long action)

Creates a broadcast pending intent that will send a media button event.

public static ComponentNamegetMediaButtonReceiverComponent(Context context)

public static KeyEventhandleIntent(android.support.v4.media.session.MediaSessionCompat mediaSessionCompat, Intent intent)

Extracts any available from an intent, passing it onto the android.support.v4.media.session.MediaSessionCompat using dispatchMediaButtonEvent, which in turn will trigger callbacks to the registered via setCallback.

protected voidonForegroundServiceStartNotAllowedException(Intent intent, ForegroundServiceStartNotAllowedException e)

This method is called when an exception is thrown when calling as a result of receiving a media button event.

public voidonReceive(Context context, Intent intent)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MediaButtonReceiver()

Methods

public void onReceive(Context context, Intent intent)

protected void onForegroundServiceStartNotAllowedException(Intent intent, ForegroundServiceStartNotAllowedException e)

This method is called when an exception is thrown when calling as a result of receiving a media button event.

By default, this method only logs the exception and it can be safely overridden. Apps that find that such a media button event has been legitimately sent, may choose to override this method and take the opportunity to post a notification from where the user journey can continue.

This exception can be thrown if a broadcast media button event is received and a media service is found in the manifest that is registered to handle . If this happens on API 31+ and the app is in the background then an exception is thrown.

Normally, a media button intent should only be required to be sent by the system in case of a Bluetooth media button event that wants to restart the app. However, in such a case the app gets an exemption and is allowed to start the foreground service. In this case this method will never be called.

In all other cases, apps should use a media browser to bind to and start the service instead of broadcasting an intent.

Parameters:

intent: The intent that was used .
e: The exception thrown by the system and caught by this broadcast receiver.

public static KeyEvent handleIntent(android.support.v4.media.session.MediaSessionCompat mediaSessionCompat, Intent intent)

Extracts any available from an intent, passing it onto the android.support.v4.media.session.MediaSessionCompat using dispatchMediaButtonEvent, which in turn will trigger callbacks to the registered via setCallback.

Parameters:

mediaSessionCompat: A android.support.v4.media.session.MediaSessionCompat that has a set.
intent: The intent to parse.

Returns:

The extracted if found, or null.

public static PendingIntent buildMediaButtonPendingIntent(Context context, long action)

Creates a broadcast pending intent that will send a media button event. The action will be translated to the appropriate , and it will be sent to the registered media button receiver in the given context. The action should be one of the following:

  • ACTION_PLAY
  • ACTION_PAUSE
  • ACTION_SKIP_TO_NEXT
  • ACTION_SKIP_TO_PREVIOUS
  • ACTION_STOP
  • ACTION_FAST_FORWARD
  • ACTION_REWIND
  • ACTION_PLAY_PAUSE

Parameters:

context: The context of the application.
action: The action to be sent via the pending intent.

Returns:

Created pending intent, or null if cannot find a unique registered media button receiver or if the action is unsupported/invalid.

public static PendingIntent buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, long action)

Creates a broadcast pending intent that will send a media button event. The action will be translated to the appropriate , and sent to the provided media button receiver via the pending intent. The action should be one of the following:

  • ACTION_PLAY
  • ACTION_PAUSE
  • ACTION_SKIP_TO_NEXT
  • ACTION_SKIP_TO_PREVIOUS
  • ACTION_STOP
  • ACTION_FAST_FORWARD
  • ACTION_REWIND
  • ACTION_PLAY_PAUSE

Parameters:

context: The context of the application.
mbrComponent: The full component name of a media button receiver where you want to send this intent.
action: The action to be sent via the pending intent.

Returns:

Created pending intent, or null if the given component name is null or the action is unsupported/invalid.

public static ComponentName getMediaButtonReceiverComponent(Context context)

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media.session;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.app.ForegroundServiceStartNotAllowedException;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction;
import android.util.Log;
import android.view.KeyEvent;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.media.MediaBrowserServiceCompat;

import java.util.List;

/**
 * A media button receiver receives and helps translate hardware media playback buttons, such as
 * those found on wired and wireless headsets, into the appropriate callbacks in your app.
 * <p />
 * You can add this MediaButtonReceiver to your app by adding it directly to your
 * AndroidManifest.xml:
 * <pre>
 * &lt;receiver android:name="androidx.media.session.MediaButtonReceiver" &gt;
 *   &lt;intent-filter&gt;
 *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
 *   &lt;/intent-filter&gt;
 * &lt;/receiver&gt;
 * </pre>
 *
 * This class assumes you have a {@link Service} in your app that controls media playback via a
 * {@link MediaSessionCompat}. Once a key event is received by MediaButtonReceiver, this class tries
 * to find a {@link Service} that can handle {@link Intent#ACTION_MEDIA_BUTTON}, and a
 * {@link MediaBrowserServiceCompat} in turn. If an appropriate service is found, this class
 * forwards the key event to the service. If neither is available or more than one valid
 * service/media browser service is found, an {@link IllegalStateException} will be thrown. Thus,
 * your app should have one of the following services to get a key event properly.
 * <p />
 *
 * <h4>Service Handling ACTION_MEDIA_BUTTON</h4>
 * A service can receive a key event by including an intent filter that handles
 * {@link Intent#ACTION_MEDIA_BUTTON}:
 * <pre>
 * &lt;service android:name="com.example.android.MediaPlaybackService" &gt;
 *   &lt;intent-filter&gt;
 *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
 *   &lt;/intent-filter&gt;
 * &lt;/service&gt;
 * </pre>
 *
 * Events can then be handled in {@link Service#onStartCommand(Intent, int, int)} by calling
 * {@link MediaButtonReceiver#handleIntent(MediaSessionCompat, Intent)}, passing in your current
 * {@link MediaSessionCompat}:
 * <pre>
 * private MediaSessionCompat mMediaSessionCompat = ...;
 *
 * public int onStartCommand(Intent intent, int flags, int startId) {
 *   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
 *   return super.onStartCommand(intent, flags, startId);
 * }
 * </pre>
 *
 * This ensures that the correct callbacks to {@link MediaSessionCompat.Callback} will be triggered
 * based on the incoming {@link KeyEvent}.
 * <p class="note"><strong>Note:</strong> Once the service is started, it must start to run in the
 * foreground.</p>
 *
 * <h4>MediaBrowserService</h4>
 * If you already have a {@link MediaBrowserServiceCompat} in your app, MediaButtonReceiver will
 * deliver the received key events to the {@link MediaBrowserServiceCompat} by default. You can
 * handle them in your {@link MediaSessionCompat.Callback}.
 */
public class MediaButtonReceiver extends BroadcastReceiver {
    private static final String TAG = "MediaButtonReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null
                || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
                || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
            Log.d(TAG, "Ignore unsupported intent: " + intent);
            return;
        }
        ComponentName mediaButtonServiceComponentName =
                getServiceComponentByAction(context, Intent.ACTION_MEDIA_BUTTON);
        if (mediaButtonServiceComponentName != null) {
            intent.setComponent(mediaButtonServiceComponentName);
            try {
                ContextCompat.startForegroundService(context, intent);
            } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) {
                if (Build.VERSION.SDK_INT >= 31
                        && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) {
                    onForegroundServiceStartNotAllowedException(intent,
                            Api31.castToForegroundServiceStartNotAllowedException(e));
                } else {
                    throw e;
                }
            }
            return;
        }
        ComponentName mediaBrowserServiceComponentName = getServiceComponentByAction(context,
                MediaBrowserServiceCompat.SERVICE_INTERFACE);
        if (mediaBrowserServiceComponentName != null) {
            PendingResult pendingResult = goAsync();
            Context applicationContext = context.getApplicationContext();
            MediaButtonConnectionCallback connectionCallback =
                    new MediaButtonConnectionCallback(applicationContext, intent, pendingResult);
            MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(applicationContext,
                    mediaBrowserServiceComponentName, connectionCallback, null);
            connectionCallback.setMediaBrowser(mediaBrowser);
            mediaBrowser.connect();
            return;
        }
        throw new IllegalStateException("Could not find any Service that handles "
                + Intent.ACTION_MEDIA_BUTTON + " or implements a media browser service.");
    }

    /**
     * This method is called when an exception is thrown when calling {@link
     * Context#startForegroundService(Intent)} as a result of receiving a media button event.
     *
     * <p>By default, this method only logs the exception and it can be safely overridden. Apps
     * that find that such a media button event has been legitimately sent, may choose to
     * override this method and take the opportunity to post a notification from where the user
     * journey can continue.
     *
     * <p>This exception can be thrown if a broadcast media button event is received and a media
     * service is found in the manifest that is registered to handle {@link
     * Intent#ACTION_MEDIA_BUTTON}. If this happens on API 31+ and the app is in the background then
     * an exception is thrown.
     *
     * <p>Normally, a media button intent should only be required to be sent by the system in case
     * of a Bluetooth media button event that wants to restart the app. However, in such a case the
     * app gets an exemption and is allowed to start the foreground service. In this case this
     * method will never be called.
     *
     * <p>In all other cases, apps should use a {@linkplain MediaBrowserCompat media browser} to
     * bind to and start the service instead of broadcasting an intent.
     *
     * @param intent The intent that was used {@linkplain Context#startForegroundService(Intent)
     *               for starting the foreground service}.
     * @param e      The exception thrown by the system and caught by this broadcast receiver.
     */
    @RequiresApi(31)
    protected void onForegroundServiceStartNotAllowedException(
            @NonNull Intent intent, @NonNull ForegroundServiceStartNotAllowedException e) {
        Log.e(
                TAG,
                "caught exception when trying to start a foreground service from the "
                        + "background: " + e.getMessage());
    }

    private static class MediaButtonConnectionCallback extends
            MediaBrowserCompat.ConnectionCallback {
        private final Context mContext;
        private final Intent mIntent;
        private final PendingResult mPendingResult;

        private MediaBrowserCompat mMediaBrowser;

        MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult) {
            mContext = context;
            mIntent = intent;
            mPendingResult = pendingResult;
        }

        void setMediaBrowser(MediaBrowserCompat mediaBrowser) {
            mMediaBrowser = mediaBrowser;
        }

        @SuppressWarnings("deprecation")
        @Override
        public void onConnected() {
            MediaControllerCompat mediaController = new MediaControllerCompat(mContext,
                    mMediaBrowser.getSessionToken());
            KeyEvent ke = mIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            mediaController.dispatchMediaButtonEvent(ke);
            finish();
        }

        @Override
        public void onConnectionSuspended() {
            finish();
        }

        @Override
        public void onConnectionFailed() {
            finish();
        }

        private void finish() {
            mMediaBrowser.disconnect();
            mPendingResult.finish();
        }
    };

    /**
     * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON}
     * intent, passing it onto the {@link MediaSessionCompat} using
     * {@link MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn
     * will trigger callbacks to the {@link MediaSessionCompat.Callback} registered via
     * {@link MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}.
     * @param mediaSessionCompat A {@link MediaSessionCompat} that has a
     *            {@link MediaSessionCompat.Callback} set.
     * @param intent The intent to parse.
     * @return The extracted {@link KeyEvent} if found, or null.
     */
    @SuppressWarnings("deprecation")
    public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
        if (mediaSessionCompat == null || intent == null
                || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
                || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
            return null;
        }
        KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
        MediaControllerCompat mediaController = mediaSessionCompat.getController();
        mediaController.dispatchMediaButtonEvent(ke);
        return ke;
    }

    /**
     * Creates a broadcast pending intent that will send a media button event. The {@code action}
     * will be translated to the appropriate {@link KeyEvent}, and it will be sent to the
     * registered media button receiver in the given context. The {@code action} should be one of
     * the following:
     * <ul>
     * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
     * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
     * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
     * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
     * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
     * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
     * </ul>
     *
     * @param context The context of the application.
     * @param action The action to be sent via the pending intent.
     * @return Created pending intent, or null if cannot find a unique registered media button
     *         receiver or if the {@code action} is unsupported/invalid.
     */
    public static PendingIntent buildMediaButtonPendingIntent(Context context,
            @MediaKeyAction long action) {
        ComponentName mbrComponent = getMediaButtonReceiverComponent(context);
        if (mbrComponent == null) {
            Log.w(TAG, "A unique media button receiver could not be found in the given context, so "
                    + "couldn't build a pending intent.");
            return null;
        }
        return buildMediaButtonPendingIntent(context, mbrComponent, action);
    }

    /**
     * Creates a broadcast pending intent that will send a media button event. The {@code action}
     * will be translated to the appropriate {@link KeyEvent}, and sent to the provided media
     * button receiver via the pending intent. The {@code action} should be one of the following:
     * <ul>
     * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
     * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
     * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
     * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
     * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
     * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
     * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
     * </ul>
     *
     * @param context The context of the application.
     * @param mbrComponent The full component name of a media button receiver where you want to send
     *            this intent.
     * @param action The action to be sent via the pending intent.
     * @return Created pending intent, or null if the given component name is null or the
     *         {@code action} is unsupported/invalid.
     */
    public static PendingIntent buildMediaButtonPendingIntent(Context context,
            ComponentName mbrComponent, @MediaKeyAction long action) {
        if (mbrComponent == null) {
            Log.w(TAG, "The component name of media button receiver should be provided.");
            return null;
        }
        int keyCode = PlaybackStateCompat.toKeyCode(action);
        if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
            Log.w(TAG,
                    "Cannot build a media button pending intent with the given action: " + action);
            return null;
        }
        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        intent.setComponent(mbrComponent);
        intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
        if (Build.VERSION.SDK_INT >= 16) {
            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        }
        return PendingIntent.getBroadcast(context, keyCode, intent,
                Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0);
    }

    /**
     */
    @RestrictTo(LIBRARY)
    public static ComponentName getMediaButtonReceiverComponent(Context context) {
        Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        queryIntent.setPackage(context.getPackageName());
        PackageManager pm = context.getPackageManager();
        List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
        if (resolveInfos.size() == 1) {
            ResolveInfo resolveInfo = resolveInfos.get(0);
            return new ComponentName(resolveInfo.activityInfo.packageName,
                    resolveInfo.activityInfo.name);
        } else if (resolveInfos.size() > 1) {
            Log.w(TAG, "More than one BroadcastReceiver that handles "
                    + Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
        }
        return null;
    }

    @SuppressWarnings("deprecation")
    private static ComponentName getServiceComponentByAction(Context context, String action) {
        PackageManager pm = context.getPackageManager();
        Intent queryIntent = new Intent(action);
        queryIntent.setPackage(context.getPackageName());
        List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, 0 /* flags */);
        if (resolveInfos.size() == 1) {
            ResolveInfo resolveInfo = resolveInfos.get(0);
            return new ComponentName(resolveInfo.serviceInfo.packageName,
                    resolveInfo.serviceInfo.name);
        } else if (resolveInfos.isEmpty()) {
            return null;
        } else {
            throw new IllegalStateException("Expected 1 service that handles " + action + ", found "
                    + resolveInfos.size());
        }
    }

    @RequiresApi(31)
    private static final class Api31 {
        /**
         * Returns true if the passed exception is a
         * {@link ForegroundServiceStartNotAllowedException}.
         */
        @DoNotInline
        public static boolean instanceOfForegroundServiceStartNotAllowedException(
                IllegalStateException e) {
            return e instanceof ForegroundServiceStartNotAllowedException;
        }

        /**
         * Casts the {@link IllegalStateException} to a
         * {@link ForegroundServiceStartNotAllowedException} and throws an exception if the cast
         * fails.
         */
        @DoNotInline
        public static ForegroundServiceStartNotAllowedException
                castToForegroundServiceStartNotAllowedException(IllegalStateException e) {
            return (ForegroundServiceStartNotAllowedException) e;
        }
    }
}