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
17package android.support.v4.media.session;
18
19import android.app.PendingIntent;
20import android.app.Service;
21import android.content.BroadcastReceiver;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.support.v4.media.MediaBrowserServiceCompat;
28import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction;
29import android.util.Log;
30import android.view.KeyEvent;
31
32import 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 */
82public class MediaButtonReceiver extends BroadcastReceiver {
83    private static final String TAG = "MediaButtonReceiver";
84
85    @Override
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     */
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     */
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     */
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
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