• Home
  • 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.support.v4.media.MediaBrowserServiceCompat;
28 import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction;
29 import android.util.Log;
30 import android.view.KeyEvent;
31 
32 import java.util.List;
33 
34 /**
35  * A media button receiver receives and helps translate hardware media playback buttons,
36  * such as those found on wired and wireless headsets, into the appropriate callbacks
37  * in your app.
38  * <p />
39  * You can add this MediaButtonReceiver to your app by adding it directly to your
40  * AndroidManifest.xml:
41  * <pre>
42  * &lt;receiver android:name="android.support.v4.media.session.MediaButtonReceiver" &gt;
43  *   &lt;intent-filter&gt;
44  *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
45  *   &lt;/intent-filter&gt;
46  * &lt;/receiver&gt;
47  * </pre>
48  * This class assumes you have a {@link Service} in your app that controls
49  * media playback via a {@link MediaSessionCompat} - all {@link Intent}s received by
50  * the MediaButtonReceiver will be forwarded to that service.
51  * <p />
52  * First priority is given to a {@link Service}
53  * that includes an intent filter that handles {@link Intent#ACTION_MEDIA_BUTTON}:
54  * <pre>
55  * &lt;service android:name="com.example.android.MediaPlaybackService" &gt;
56  *   &lt;intent-filter&gt;
57  *     &lt;action android:name="android.intent.action.MEDIA_BUTTON" /&gt;
58  *   &lt;/intent-filter&gt;
59  * &lt;/service&gt;
60  * </pre>
61  *
62  * If such a {@link Service} is not found, MediaButtonReceiver will attempt to
63  * find a media browser service implementation.
64  * If neither is available or more than one valid service/media browser service is found, an
65  * {@link IllegalStateException} will be thrown.
66  * <p />
67  * Events can then be handled in {@link Service#onStartCommand(Intent, int, int)} by calling
68  * {@link MediaButtonReceiver#handleIntent(MediaSessionCompat, Intent)}, passing in
69  * your current {@link MediaSessionCompat}:
70  * <pre>
71  * private MediaSessionCompat mMediaSessionCompat = ...;
72  *
73  * public int onStartCommand(Intent intent, int flags, int startId) {
74  *   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
75  *   return super.onStartCommand(intent, flags, startId);
76  * }
77  * </pre>
78  *
79  * This ensures that the correct callbacks to {@link MediaSessionCompat.Callback}
80  * will be triggered based on the incoming {@link KeyEvent}.
81  */
82 public class MediaButtonReceiver extends BroadcastReceiver {
83     private static final String TAG = "MediaButtonReceiver";
84 
85     @Override
onReceive(Context context, Intent intent)86     public void onReceive(Context context, Intent intent) {
87         Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
88         queryIntent.setPackage(context.getPackageName());
89         PackageManager pm = context.getPackageManager();
90         List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, 0);
91         if (resolveInfos.isEmpty()) {
92             // Fall back to looking for any available media browser service
93             queryIntent.setAction(MediaBrowserServiceCompat.SERVICE_INTERFACE);
94             resolveInfos = pm.queryIntentServices(queryIntent, 0);
95         }
96         if (resolveInfos.isEmpty()) {
97             throw new IllegalStateException("Could not find any Service that handles " +
98                     Intent.ACTION_MEDIA_BUTTON + " or a media browser service implementation");
99         } else if (resolveInfos.size() != 1) {
100             throw new IllegalStateException("Expected 1 Service that handles " +
101                     queryIntent.getAction() + ", found " + resolveInfos.size() );
102         }
103         ResolveInfo resolveInfo = resolveInfos.get(0);
104         ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
105                 resolveInfo.serviceInfo.name);
106         intent.setComponent(componentName);
107         context.startService(intent);
108     }
109 
110     /**
111      * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON}
112      * intent, passing it onto the {@link MediaSessionCompat} using
113      * {@link MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn
114      * will trigger callbacks to the {@link MediaSessionCompat.Callback} registered via
115      * {@link MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}.
116      * <p />
117      * The returned {@link KeyEvent} is non-null if any {@link KeyEvent} is found and can
118      * be used if any additional processing is needed beyond what is done in the
119      * {@link MediaSessionCompat.Callback}. An example of is to prevent redelivery of a
120      * {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} Intent in the case of the Service being
121      * restarted (which, by default, will redeliver the last received Intent).
122      * <pre>
123      * KeyEvent keyEvent = MediaButtonReceiver.handleIntent(mediaSession, intent);
124      * if (keyEvent != null && keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
125      *   Intent emptyIntent = new Intent(intent);
126      *   emptyIntent.setAction("");
127      *   startService(emptyIntent);
128      * }
129      * </pre>
130      * @param mediaSessionCompat A {@link MediaSessionCompat} that has a
131      *            {@link MediaSessionCompat.Callback} set.
132      * @param intent The intent to parse.
133      * @return The extracted {@link KeyEvent} if found, or null.
134      */
handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent)135     public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
136         if (mediaSessionCompat == null || intent == null
137                 || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
138                 || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
139             return null;
140         }
141         KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
142         MediaControllerCompat mediaController = mediaSessionCompat.getController();
143         mediaController.dispatchMediaButtonEvent(ke);
144         return ke;
145     }
146 
147     /**
148      * Creates a broadcast pending intent that will send a media button event. The {@code action}
149      * will be translated to the appropriate {@link KeyEvent}, and it will be sent to the
150      * registered media button receiver in the given context. The {@code action} should be one of
151      * the following:
152      * <ul>
153      * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
154      * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
155      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
156      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
157      * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
158      * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
159      * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
160      * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
161      * </ul>
162      *
163      * @param context The context of the application.
164      * @param action The action to be sent via the pending intent.
165      * @return Created pending intent, or null if cannot find a unique registered media button
166      *         receiver or if the {@code action} is unsupported/invalid.
167      */
buildMediaButtonPendingIntent(Context context, @MediaKeyAction long action)168     public static PendingIntent buildMediaButtonPendingIntent(Context context,
169             @MediaKeyAction long action) {
170         ComponentName mbrComponent = getMediaButtonReceiverComponent(context);
171         if (mbrComponent == null) {
172             Log.w(TAG, "A unique media button receiver could not be found in the given context, so "
173                     + "couldn't build a pending intent.");
174             return null;
175         }
176         return buildMediaButtonPendingIntent(context, mbrComponent, action);
177     }
178 
179     /**
180      * Creates a broadcast pending intent that will send a media button event. The {@code action}
181      * will be translated to the appropriate {@link KeyEvent}, and sent to the provided media
182      * button receiver via the pending intent. The {@code action} should be one of the following:
183      * <ul>
184      * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li>
185      * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li>
186      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
187      * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
188      * <li>{@link PlaybackStateCompat#ACTION_STOP}</li>
189      * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
190      * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li>
191      * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li>
192      * </ul>
193      *
194      * @param context The context of the application.
195      * @param mbrComponent The full component name of a media button receiver where you want to send
196      *            this intent.
197      * @param action The action to be sent via the pending intent.
198      * @return Created pending intent, or null if the given component name is null or the
199      *         {@code action} is unsupported/invalid.
200      */
buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, @MediaKeyAction long action)201     public static PendingIntent buildMediaButtonPendingIntent(Context context,
202             ComponentName mbrComponent, @MediaKeyAction long action) {
203         if (mbrComponent == null) {
204             Log.w(TAG, "The component name of media button receiver should be provided.");
205             return null;
206         }
207         int keyCode = PlaybackStateCompat.toKeyCode(action);
208         if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
209             Log.w(TAG,
210                     "Cannot build a media button pending intent with the given action: " + action);
211             return null;
212         }
213         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
214         intent.setComponent(mbrComponent);
215         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
216         return PendingIntent.getBroadcast(context, keyCode, intent, 0);
217     }
218 
getMediaButtonReceiverComponent(Context context)219     static ComponentName getMediaButtonReceiverComponent(Context context) {
220         Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
221         queryIntent.setPackage(context.getPackageName());
222         PackageManager pm = context.getPackageManager();
223         List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
224         if (resolveInfos.size() == 1) {
225             ResolveInfo resolveInfo = resolveInfos.get(0);
226             return new ComponentName(resolveInfo.activityInfo.packageName,
227                     resolveInfo.activityInfo.name);
228         } else if (resolveInfos.size() > 1) {
229             Log.w(TAG, "More than one BroadcastReceiver that handles "
230                     + Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
231         }
232         return null;
233     }
234 }
235