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