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 * <receiver android:name="androidx.media.session.MediaButtonReceiver" > 52 * <intent-filter> 53 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 54 * </intent-filter> 55 * </receiver> 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 * <service android:name="com.example.android.MediaPlaybackService" > 72 * <intent-filter> 73 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 74 * </intent-filter> 75 * </service> 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 onReceive(Context context, Intent intent)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 MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult)143 MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult) { 144 mContext = context; 145 mIntent = intent; 146 mPendingResult = pendingResult; 147 } 148 setMediaBrowser(MediaBrowserCompat mediaBrowser)149 void setMediaBrowser(MediaBrowserCompat mediaBrowser) { 150 mMediaBrowser = mediaBrowser; 151 } 152 153 @Override onConnected()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 onConnectionSuspended()167 public void onConnectionSuspended() { 168 finish(); 169 } 170 171 @Override onConnectionFailed()172 public void onConnectionFailed() { 173 finish(); 174 } 175 finish()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 */ handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent)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 */ buildMediaButtonPendingIntent(Context context, @MediaKeyAction long action)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 */ buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, @MediaKeyAction long action)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) getMediaButtonReceiverComponent(Context context)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 startForegroundService(Context context, Intent intent)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 getServiceComponentByAction(Context context, String action)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