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 * <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" > 43 * <intent-filter> 44 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 45 * </intent-filter> 46 * </receiver> 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 * <service android:name="com.example.android.MediaPlaybackService" > 56 * <intent-filter> 57 * <action android:name="android.intent.action.MEDIA_BUTTON" /> 58 * </intent-filter> 59 * </service> 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