Java tutorial
/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media.session; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; import android.os.RemoteException; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction; import android.util.Log; import android.view.KeyEvent; import androidx.annotation.RestrictTo; import androidx.media.MediaBrowserServiceCompat; import java.util.List; /** * A media button receiver receives and helps translate hardware media playback buttons, such as * those found on wired and wireless headsets, into the appropriate callbacks in your app. * <p /> * You can add this MediaButtonReceiver to your app by adding it directly to your * AndroidManifest.xml: * <pre> * <receiver android:name="androidx.media.session.MediaButtonReceiver" > * <intent-filter> * <action android:name="android.intent.action.MEDIA_BUTTON" /> * </intent-filter> * </receiver> * </pre> * * This class assumes you have a {@link Service} in your app that controls media playback via a * {@link MediaSessionCompat}. Once a key event is received by MediaButtonReceiver, this class tries * to find a {@link Service} that can handle {@link Intent#ACTION_MEDIA_BUTTON}, and a * {@link MediaBrowserServiceCompat} in turn. If an appropriate service is found, this class * forwards the key event to the service. If neither is available or more than one valid * service/media browser service is found, an {@link IllegalStateException} will be thrown. Thus, * your app should have one of the following services to get a key event properly. * <p /> * * <h4>Service Handling ACTION_MEDIA_BUTTON</h4> * A service can receive a key event by including an intent filter that handles * {@link Intent#ACTION_MEDIA_BUTTON}: * <pre> * <service android:name="com.example.android.MediaPlaybackService" > * <intent-filter> * <action android:name="android.intent.action.MEDIA_BUTTON" /> * </intent-filter> * </service> * </pre> * * Events can then be handled in {@link Service#onStartCommand(Intent, int, int)} by calling * {@link MediaButtonReceiver#handleIntent(MediaSessionCompat, Intent)}, passing in your current * {@link MediaSessionCompat}: * <pre> * private MediaSessionCompat mMediaSessionCompat = ...; * * public int onStartCommand(Intent intent, int flags, int startId) { * MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent); * return super.onStartCommand(intent, flags, startId); * } * </pre> * * This ensures that the correct callbacks to {@link MediaSessionCompat.Callback} will be triggered * based on the incoming {@link KeyEvent}. * <p class="note"><strong>Note:</strong> Once the service is started, it must start to run in the * foreground.</p> * * <h4>MediaBrowserService</h4> * If you already have a {@link MediaBrowserServiceCompat} in your app, MediaButtonReceiver will * deliver the received key events to the {@link MediaBrowserServiceCompat} by default. You can * handle them in your {@link MediaSessionCompat.Callback}. */ public class MediaButtonReceiver extends BroadcastReceiver { private static final String TAG = "MediaButtonReceiver"; @Override public void onReceive(Context context, Intent intent) { if (intent == null || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) { Log.d(TAG, "Ignore unsupported intent: " + intent); return; } ComponentName mediaButtonServiceComponentName = getServiceComponentByAction(context, Intent.ACTION_MEDIA_BUTTON); if (mediaButtonServiceComponentName != null) { intent.setComponent(mediaButtonServiceComponentName); startForegroundService(context, intent); return; } ComponentName mediaBrowserServiceComponentName = getServiceComponentByAction(context, MediaBrowserServiceCompat.SERVICE_INTERFACE); if (mediaBrowserServiceComponentName != null) { PendingResult pendingResult = goAsync(); Context applicationContext = context.getApplicationContext(); MediaButtonConnectionCallback connectionCallback = new MediaButtonConnectionCallback(applicationContext, intent, pendingResult); MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(applicationContext, mediaBrowserServiceComponentName, connectionCallback, null); connectionCallback.setMediaBrowser(mediaBrowser); mediaBrowser.connect(); return; } throw new IllegalStateException("Could not find any Service that handles " + Intent.ACTION_MEDIA_BUTTON + " or implements a media browser service."); } private static class MediaButtonConnectionCallback extends MediaBrowserCompat.ConnectionCallback { private final Context mContext; private final Intent mIntent; private final PendingResult mPendingResult; private MediaBrowserCompat mMediaBrowser; MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult) { mContext = context; mIntent = intent; mPendingResult = pendingResult; } void setMediaBrowser(MediaBrowserCompat mediaBrowser) { mMediaBrowser = mediaBrowser; } @Override public void onConnected() { try { MediaControllerCompat mediaController = new MediaControllerCompat(mContext, mMediaBrowser.getSessionToken()); KeyEvent ke = mIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); mediaController.dispatchMediaButtonEvent(ke); } catch (RemoteException e) { Log.e(TAG, "Failed to create a media controller", e); } finish(); } @Override public void onConnectionSuspended() { finish(); } @Override public void onConnectionFailed() { finish(); } private void finish() { mMediaBrowser.disconnect(); mPendingResult.finish(); } }; /** * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON} * intent, passing it onto the {@link MediaSessionCompat} using * {@link MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn * will trigger callbacks to the {@link MediaSessionCompat.Callback} registered via * {@link MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}. * @param mediaSessionCompat A {@link MediaSessionCompat} that has a * {@link MediaSessionCompat.Callback} set. * @param intent The intent to parse. * @return The extracted {@link KeyEvent} if found, or null. */ public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) { if (mediaSessionCompat == null || intent == null || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) { return null; } KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); MediaControllerCompat mediaController = mediaSessionCompat.getController(); mediaController.dispatchMediaButtonEvent(ke); return ke; } /** * Creates a broadcast pending intent that will send a media button event. The {@code action} * will be translated to the appropriate {@link KeyEvent}, and it will be sent to the * registered media button receiver in the given context. The {@code action} should be one of * the following: * <ul> * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li> * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li> * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li> * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li> * <li>{@link PlaybackStateCompat#ACTION_STOP}</li> * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li> * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li> * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li> * </ul> * * @param context The context of the application. * @param action The action to be sent via the pending intent. * @return Created pending intent, or null if cannot find a unique registered media button * receiver or if the {@code action} is unsupported/invalid. */ public static PendingIntent buildMediaButtonPendingIntent(Context context, @MediaKeyAction long action) { ComponentName mbrComponent = getMediaButtonReceiverComponent(context); if (mbrComponent == null) { Log.w(TAG, "A unique media button receiver could not be found in the given context, so " + "couldn't build a pending intent."); return null; } return buildMediaButtonPendingIntent(context, mbrComponent, action); } /** * Creates a broadcast pending intent that will send a media button event. The {@code action} * will be translated to the appropriate {@link KeyEvent}, and sent to the provided media * button receiver via the pending intent. The {@code action} should be one of the following: * <ul> * <li>{@link PlaybackStateCompat#ACTION_PLAY}</li> * <li>{@link PlaybackStateCompat#ACTION_PAUSE}</li> * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li> * <li>{@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li> * <li>{@link PlaybackStateCompat#ACTION_STOP}</li> * <li>{@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li> * <li>{@link PlaybackStateCompat#ACTION_REWIND}</li> * <li>{@link PlaybackStateCompat#ACTION_PLAY_PAUSE}</li> * </ul> * * @param context The context of the application. * @param mbrComponent The full component name of a media button receiver where you want to send * this intent. * @param action The action to be sent via the pending intent. * @return Created pending intent, or null if the given component name is null or the * {@code action} is unsupported/invalid. */ public static PendingIntent buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, @MediaKeyAction long action) { if (mbrComponent == null) { Log.w(TAG, "The component name of media button receiver should be provided."); return null; } int keyCode = PlaybackStateCompat.toKeyCode(action); if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { Log.w(TAG, "Cannot build a media button pending intent with the given action: " + action); return null; } Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.setComponent(mbrComponent); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); return PendingIntent.getBroadcast(context, keyCode, intent, 0); } /** * @hide */ @RestrictTo(LIBRARY) public static ComponentName getMediaButtonReceiverComponent(Context context) { Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); queryIntent.setPackage(context.getPackageName()); PackageManager pm = context.getPackageManager(); List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0); if (resolveInfos.size() == 1) { ResolveInfo resolveInfo = resolveInfos.get(0); return new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); } else if (resolveInfos.size() > 1) { Log.w(TAG, "More than one BroadcastReceiver that handles " + Intent.ACTION_MEDIA_BUTTON + " was found, returning null."); } return null; } private static void startForegroundService(Context context, Intent intent) { if (Build.VERSION.SDK_INT >= 26) { context.startForegroundService(intent); } else { context.startService(intent); } } private static ComponentName getServiceComponentByAction(Context context, String action) { PackageManager pm = context.getPackageManager(); Intent queryIntent = new Intent(action); queryIntent.setPackage(context.getPackageName()); List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, 0 /* flags */); if (resolveInfos.size() == 1) { ResolveInfo resolveInfo = resolveInfos.get(0); return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); } else if (resolveInfos.isEmpty()) { return null; } else { throw new IllegalStateException( "Expected 1 service that handles " + action + ", found " + resolveInfos.size()); } } }