Home | History | Annotate | Download | only in session
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.media.session;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
     20 
     21 import android.app.PendingIntent;
     22 import android.app.Service;
     23 import android.content.BroadcastReceiver;
     24 import android.content.ComponentName;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.PackageManager;
     28 import android.content.pm.ResolveInfo;
     29 import android.os.Build;
     30 import android.os.RemoteException;
     31 import android.support.v4.media.MediaBrowserCompat;
     32 import android.support.v4.media.session.MediaControllerCompat;
     33 import android.support.v4.media.session.MediaSessionCompat;
     34 import android.support.v4.media.session.PlaybackStateCompat;
     35 import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction;
     36 import android.util.Log;
     37 import android.view.KeyEvent;
     38 
     39 import androidx.annotation.RestrictTo;
     40 import androidx.media.MediaBrowserServiceCompat;
     41 
     42 import java.util.List;
     43 
     44 /**
     45  * A media button receiver receives and helps translate hardware media playback buttons, such as
     46  * those found on wired and wireless headsets, into the appropriate callbacks in your app.
     47  * <p />
     48  * You can add this MediaButtonReceiver to your app by adding it directly to your
     49  * AndroidManifest.xml:
     50  * <pre>
     51  * &lt;receiver android:name="androidx.media.session.MediaButtonReceiver" &gt;
     52  *   &lt;intent-filter&gt;
     53  *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
     54  *   &lt;/intent-filter&gt;
     55  * &lt;/receiver&gt;
     56  * </pre>
     57  *
     58  * This class assumes you have a {@link Service} in your app that controls media playback via a
     59  * {@link MediaSessionCompat}. Once a key event is received by MediaButtonReceiver, this class tries
     60  * to find a {@link Service} that can handle {@link Intent#ACTION_MEDIA_BUTTON}, and a
     61  * {@link MediaBrowserServiceCompat} in turn. If an appropriate service is found, this class
     62  * forwards the key event to the service. If neither is available or more than one valid
     63  * service/media browser service is found, an {@link IllegalStateException} will be thrown. Thus,
     64  * your app should have one of the following services to get a key event properly.
     65  * <p />
     66  *
     67  * <h4>Service Handling ACTION_MEDIA_BUTTON</h4>
     68  * A service can receive a key event by including an intent filter that handles
     69  * {@link Intent#ACTION_MEDIA_BUTTON}:
     70  * <pre>
     71  * &lt;service android:name="com.example.android.MediaPlaybackService" &gt;
     72  *   &lt;intent-filter&gt;
     73  *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
     74  *   &lt;/intent-filter&gt;
     75  * &lt;/service&gt;
     76  * </pre>
     77  *
     78  * Events can then be handled in {@link Service#onStartCommand(Intent, int, int)} by calling
     79  * {@link MediaButtonReceiver#handleIntent(MediaSessionCompat, Intent)}, passing in your current
     80  * {@link MediaSessionCompat}:
     81  * <pre>
     82  * private MediaSessionCompat mMediaSessionCompat = ...;
     83  *
     84  * public int onStartCommand(Intent intent, int flags, int startId) {
     85  *   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
     86  *   return super.onStartCommand(intent, flags, startId);
     87  * }
     88  * </pre>
     89  *
     90  * This ensures that the correct callbacks to {@link MediaSessionCompat.Callback} will be triggered
     91  * based on the incoming {@link KeyEvent}.
     92  * <p class="note"><strong>Note:</strong> Once the service is started, it must start to run in the
     93  * foreground.</p>
     94  *
     95  * <h4>MediaBrowserService</h4>
     96  * If you already have a {@link MediaBrowserServiceCompat} in your app, MediaButtonReceiver will
     97  * deliver the received key events to the {@link MediaBrowserServiceCompat} by default. You can
     98  * handle them in your {@link MediaSessionCompat.Callback}.
     99  */
    100 public class MediaButtonReceiver extends BroadcastReceiver {
    101     private static final String TAG = "MediaButtonReceiver";
    102 
    103     @Override
    104     public void onReceive(Context context, Intent intent) {
    105         if (intent == null
    106                 || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
    107                 || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
    108             Log.d(TAG, "Ignore unsupported intent: " + intent);
    109             return;
    110         }
    111         ComponentName mediaButtonServiceComponentName =
    112                 getServiceComponentByAction(context, Intent.ACTION_MEDIA_BUTTON);
    113         if (mediaButtonServiceComponentName != null) {
    114             intent.setComponent(mediaButtonServiceComponentName);
    115             startForegroundService(context, intent);
    116             return;
    117         }
    118         ComponentName mediaBrowserServiceComponentName = getServiceComponentByAction(context,
    119                 MediaBrowserServiceCompat.SERVICE_INTERFACE);
    120         if (mediaBrowserServiceComponentName != null) {
    121             PendingResult pendingResult = goAsync();
    122             Context applicationContext = context.getApplicationContext();
    123             MediaButtonConnectionCallback connectionCallback =
    124                     new MediaButtonConnectionCallback(applicationContext, intent, pendingResult);
    125             MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(applicationContext,
    126                     mediaBrowserServiceComponentName, connectionCallback, null);
    127             connectionCallback.setMediaBrowser(mediaBrowser);
    128             mediaBrowser.connect();
    129             return;
    130         }
    131         throw new IllegalStateException("Could not find any Service that handles "
    132                 + Intent.ACTION_MEDIA_BUTTON + " or implements a media browser service.");
    133     }
    134 
    135     private static class MediaButtonConnectionCallback extends
    136             MediaBrowserCompat.ConnectionCallback {
    137         private final Context mContext;
    138         private final Intent mIntent;
    139         private final PendingResult mPendingResult;
    140 
    141         private MediaBrowserCompat mMediaBrowser;
    142 
    143         MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult) {
    144             mContext = context;
    145             mIntent = intent;
    146             mPendingResult = pendingResult;
    147         }
    148 
    149         void setMediaBrowser(MediaBrowserCompat mediaBrowser) {
    150             mMediaBrowser = mediaBrowser;
    151         }
    152 
    153         @Override
    154         public void onConnected() {
    155             try {
    156                 MediaControllerCompat mediaController = new MediaControllerCompat(mContext,
    157                         mMediaBrowser.getSessionToken());
    158                 KeyEvent ke = mIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
    159                 mediaController.dispatchMediaButtonEvent(ke);
    160             } catch (RemoteException e) {
    161                 Log.e(TAG, "Failed to create a media controller", e);
    162             }
    163             finish();
    164         }
    165 
    166         @Override
    167         public void onConnectionSuspended() {
    168             finish();
    169         }
    170 
    171         @Override
    172         public void onConnectionFailed() {
    173             finish();
    174         }
    175 
    176         private void finish() {
    177             mMediaBrowser.disconnect();
    178             mPendingResult.finish();
    179         }
    180     };
    181 
    182     /**
    183      * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON}
    184      * intent, passing it onto the {@link MediaSessionCompat} using
    185      * {@link MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn
    186      * will trigger callbacks to the {@link MediaSessionCompat.Callback} registered via
    187      * {@link MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}.
    188      * @param mediaSessionCompat A {@link MediaSessionCompat} that has a
    189      *            {@link MediaSessionCompat.Callback} set.
    190      * @param intent The intent to parse.
    191      * @return The extracted {@link KeyEvent} if found, or null.
    192      */
    193     public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
    194         if (mediaSessionCompat == null || intent == null
    195                 || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
    196                 || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
    197             return null;
    198         }
    199         KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
    200         MediaControllerCompat mediaController = mediaSessionCompat.getController();
    201         mediaController.dispatchMediaButtonEvent(ke);
    202         return ke;
    203     }
    204 
    205     /**
    206      * Creates a broadcast pending intent that will send a media button event. The {@code action}
    207      * will be translated to the appropriate {@link KeyEvent}, and it will be sent to the
    208      * registered media button receiver in the given context. The {@code action} should be one of
    209      * the following:
    210      * <ul>
    211      * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
    212      * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
    213      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
    214      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
    215      * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
    216      * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
    217      * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
    218      * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
    219      * </ul>
    220      *
    221      * @param context The context of the application.
    222      * @param action The action to be sent via the pending intent.
    223      * @return Created pending intent, or null if cannot find a unique registered media button
    224      *         receiver or if the {@code action} is unsupported/invalid.
    225      */
    226     public static PendingIntent buildMediaButtonPendingIntent(Context context,
    227             @MediaKeyAction long action) {
    228         ComponentName mbrComponent = getMediaButtonReceiverComponent(context);
    229         if (mbrComponent == null) {
    230             Log.w(TAG, "A unique media button receiver could not be found in the given context, so "
    231                     + "couldn't build a pending intent.");
    232             return null;
    233         }
    234         return buildMediaButtonPendingIntent(context, mbrComponent, action);
    235     }
    236 
    237     /**
    238      * Creates a broadcast pending intent that will send a media button event. The {@code action}
    239      * will be translated to the appropriate {@link KeyEvent}, and sent to the provided media
    240      * button receiver via the pending intent. The {@code action} should be one of the following:
    241      * <ul>
    242      * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
    243      * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
    244      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
    245      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
    246      * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
    247      * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
    248      * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
    249      * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
    250      * </ul>
    251      *
    252      * @param context The context of the application.
    253      * @param mbrComponent The full component name of a media button receiver where you want to send
    254      *            this intent.
    255      * @param action The action to be sent via the pending intent.
    256      * @return Created pending intent, or null if the given component name is null or the
    257      *         {@code action} is unsupported/invalid.
    258      */
    259     public static PendingIntent buildMediaButtonPendingIntent(Context context,
    260             ComponentName mbrComponent, @MediaKeyAction long action) {
    261         if (mbrComponent == null) {
    262             Log.w(TAG, "The component name of media button receiver should be provided.");
    263             return null;
    264         }
    265         int keyCode = PlaybackStateCompat.toKeyCode(action);
    266         if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
    267             Log.w(TAG,
    268                     "Cannot build a media button pending intent with the given action: " + action);
    269             return null;
    270         }
    271         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    272         intent.setComponent(mbrComponent);
    273         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
    274         return PendingIntent.getBroadcast(context, keyCode, intent, 0);
    275     }
    276 
    277     /**
    278      * @hide
    279      */
    280     @RestrictTo(LIBRARY)
    281     public static ComponentName getMediaButtonReceiverComponent(Context context) {
    282         Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    283         queryIntent.setPackage(context.getPackageName());
    284         PackageManager pm = context.getPackageManager();
    285         List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
    286         if (resolveInfos.size() == 1) {
    287             ResolveInfo resolveInfo = resolveInfos.get(0);
    288             return new ComponentName(resolveInfo.activityInfo.packageName,
    289                     resolveInfo.activityInfo.name);
    290         } else if (resolveInfos.size() > 1) {
    291             Log.w(TAG, "More than one BroadcastReceiver that handles "
    292                     + Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
    293         }
    294         return null;
    295     }
    296 
    297     private static void startForegroundService(Context context, Intent intent) {
    298         if (Build.VERSION.SDK_INT >= 26) {
    299             context.startForegroundService(intent);
    300         } else {
    301             context.startService(intent);
    302         }
    303     }
    304 
    305     private static ComponentName getServiceComponentByAction(Context context, String action) {
    306         PackageManager pm = context.getPackageManager();
    307         Intent queryIntent = new Intent(action);
    308         queryIntent.setPackage(context.getPackageName());
    309         List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, 0 /* flags */);
    310         if (resolveInfos.size() == 1) {
    311             ResolveInfo resolveInfo = resolveInfos.get(0);
    312             return new ComponentName(resolveInfo.serviceInfo.packageName,
    313                     resolveInfo.serviceInfo.name);
    314         } else if (resolveInfos.isEmpty()) {
    315             return null;
    316         } else {
    317             throw new IllegalStateException("Expected 1 service that handles " + action + ", found "
    318                     + resolveInfos.size());
    319         }
    320     }
    321 }
    322