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 * <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" > 45 * <intent-filter> 46 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 47 * </intent-filter> 48 * </receiver> 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 * <service android:name="com.example.android.MediaPlaybackService" > 65 * <intent-filter> 66 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 67 * </intent-filter> 68 * </service> 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