| /* |
| * Copyright (C) 2014 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 android.support.v4.media.session; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY; |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID; |
| import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_UID; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.media.AudioAttributes; |
| import android.media.AudioManager; |
| import android.media.MediaDescription; |
| import android.media.MediaMetadata; |
| import android.media.MediaMetadataEditor; |
| import android.media.MediaMetadataRetriever; |
| import android.media.Rating; |
| import android.media.RemoteControlClient; |
| import android.media.VolumeProvider; |
| import android.media.session.MediaSession; |
| import android.media.session.PlaybackState; |
| import android.net.Uri; |
| import android.os.BadParcelableException; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.os.SystemClock; |
| import android.support.v4.media.MediaDescriptionCompat; |
| import android.support.v4.media.MediaMetadataCompat; |
| import android.support.v4.media.RatingCompat; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.KeyEvent; |
| import android.view.ViewConfiguration; |
| |
| import androidx.annotation.DoNotInline; |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.RestrictTo; |
| import androidx.core.app.BundleCompat; |
| import androidx.media.MediaSessionManager; |
| import androidx.media.MediaSessionManager.RemoteUserInfo; |
| import androidx.media.VolumeProviderCompat; |
| import androidx.media.session.MediaButtonReceiver; |
| import androidx.versionedparcelable.ParcelUtils; |
| import androidx.versionedparcelable.VersionedParcelable; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.WeakReference; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * Allows interaction with media controllers, volume keys, media buttons, and |
| * transport controls. |
| * <p> |
| * A MediaSession should be created when an app wants to publish media playback |
| * information or handle media keys. In general an app only needs one session |
| * for all playback, though multiple sessions can be created to provide finer |
| * grain controls of media. |
| * <p> |
| * Once a session is created the owner of the session may pass its |
| * {@link #getSessionToken() session token} to other processes to allow them to |
| * create a {@link MediaControllerCompat} to interact with the session. |
| * <p> |
| * To receive commands, media keys, and other events a {@link Callback} must be |
| * set with {@link #setCallback(Callback)}. |
| * <p> |
| * When an app is finished performing playback it must call {@link #release()} |
| * to clean up the session and notify any controllers. |
| * <p> |
| * MediaSessionCompat objects are not thread safe and all calls should be made |
| * from the same thread. |
| * <p> |
| * This is a helper for accessing features in |
| * {@link android.media.session.MediaSession} introduced after API level 4 in a |
| * backwards compatible fashion. |
| * |
| * <div class="special reference"> |
| * <h3>Developer Guides</h3> |
| * <p>For information about building your media application, read the |
| * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p> |
| * </div> |
| */ |
| public class MediaSessionCompat { |
| static final String TAG = "MediaSessionCompat"; |
| |
| private final MediaSessionImpl mImpl; |
| private final MediaControllerCompat mController; |
| private final ArrayList<OnActiveChangeListener> mActiveListeners = new ArrayList<>(); |
| |
| @IntDef(flag=true, value={ |
| FLAG_HANDLES_MEDIA_BUTTONS, |
| FLAG_HANDLES_TRANSPORT_CONTROLS, |
| FLAG_HANDLES_QUEUE_COMMANDS }) |
| @Retention(RetentionPolicy.SOURCE) |
| private @interface SessionFlags {} |
| |
| /** |
| * Sets this flag on the session to indicate that it can handle media button |
| * events. |
| * |
| * @deprecated This flag is no longer used. All media sessions are expected to handle media |
| * button events now. For backward compatibility, this flag will be always set. |
| */ |
| @Deprecated |
| public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; |
| |
| /** |
| * Sets this flag on the session to indicate that it handles transport |
| * control commands through its {@link Callback}. |
| * |
| * @deprecated This flag is no longer used. All media sessions are expected to handle transport |
| * controls now. For backward compatibility, this flag will be always set. |
| */ |
| @Deprecated |
| public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; |
| |
| /** |
| * Sets this flag on the session to indicate that it handles queue |
| * management commands through its {@link Callback}. |
| */ |
| public static final int FLAG_HANDLES_QUEUE_COMMANDS = 1 << 2; |
| |
| /** |
| * Predefined custom action to flag the media that is currently playing as inappropriate. |
| * |
| * @see Callback#onCustomAction |
| */ |
| public static final String ACTION_FLAG_AS_INAPPROPRIATE = |
| "android.support.v4.media.session.action.FLAG_AS_INAPPROPRIATE"; |
| |
| /** |
| * Predefined custom action to skip the advertisement that is currently playing. |
| * |
| * @see Callback#onCustomAction |
| */ |
| public static final String ACTION_SKIP_AD = "android.support.v4.media.session.action.SKIP_AD"; |
| |
| /** |
| * Predefined custom action to follow an artist, album, or playlist. The extra bundle must have |
| * {@link #ARGUMENT_MEDIA_ATTRIBUTE} to indicate the type of the follow action. The |
| * bundle can also have an optional string argument, |
| * {@link #ARGUMENT_MEDIA_ATTRIBUTE_VALUE}, to specify the target to follow (e.g., the |
| * name of the artist to follow). If this argument is omitted, the currently playing media will |
| * be the target of the action. Thus, the session must perform the follow action with the |
| * current metadata. If there's no specified attribute in the current metadata, the controller |
| * must not omit this argument. |
| * |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE_VALUE |
| * @see Callback#onCustomAction |
| */ |
| public static final String ACTION_FOLLOW = "android.support.v4.media.session.action.FOLLOW"; |
| |
| /** |
| * Predefined custom action to unfollow an artist, album, or playlist. The extra bundle must |
| * have {@link #ARGUMENT_MEDIA_ATTRIBUTE} to indicate the type of the unfollow action. |
| * The bundle can also have an optional string argument, |
| * {@link #ARGUMENT_MEDIA_ATTRIBUTE_VALUE}, to specify the target to unfollow (e.g., the |
| * name of the artist to unfollow). If this argument is omitted, the currently playing media |
| * will be the target of the action. Thus, the session must perform the unfollow action with the |
| * current metadata. If there's no specified attribute in the current metadata, the controller |
| * must not omit this argument. |
| * |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE_VALUE |
| * @see Callback#onCustomAction |
| */ |
| public static final String ACTION_UNFOLLOW = "android.support.v4.media.session.action.UNFOLLOW"; |
| |
| /** |
| * Argument to indicate the media attribute. It should be one of the following: |
| * <ul> |
| * <li>{@link #MEDIA_ATTRIBUTE_ARTIST}</li> |
| * <li>{@link #MEDIA_ATTRIBUTE_PLAYLIST}</li> |
| * <li>{@link #MEDIA_ATTRIBUTE_ALBUM}</li> |
| * </ul> |
| */ |
| public static final String ARGUMENT_MEDIA_ATTRIBUTE = |
| "android.support.v4.media.session.ARGUMENT_MEDIA_ATTRIBUTE"; |
| |
| /** |
| * String argument to indicate the value of the media attribute (e.g., the name of the artist). |
| */ |
| public static final String ARGUMENT_MEDIA_ATTRIBUTE_VALUE = |
| "android.support.v4.media.session.ARGUMENT_MEDIA_ATTRIBUTE_VALUE"; |
| |
| /** |
| * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the artist. |
| * |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE |
| */ |
| public static final int MEDIA_ATTRIBUTE_ARTIST = 0; |
| |
| /** |
| * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the album. |
| * |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE |
| */ |
| public static final int MEDIA_ATTRIBUTE_ALBUM = 1; |
| |
| /** |
| * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the playlist. |
| * |
| * @see #ARGUMENT_MEDIA_ATTRIBUTE |
| */ |
| public static final int MEDIA_ATTRIBUTE_PLAYLIST = 2; |
| |
| /** |
| * Custom action to invoke playFromUri() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_PLAY_FROM_URI = |
| "android.support.v4.media.session.action.PLAY_FROM_URI"; |
| |
| /** |
| * Custom action to invoke prepare() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_PREPARE = "android.support.v4.media.session.action.PREPARE"; |
| |
| /** |
| * Custom action to invoke prepareFromMediaId() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_PREPARE_FROM_MEDIA_ID = |
| "android.support.v4.media.session.action.PREPARE_FROM_MEDIA_ID"; |
| |
| /** |
| * Custom action to invoke prepareFromSearch() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_PREPARE_FROM_SEARCH = |
| "android.support.v4.media.session.action.PREPARE_FROM_SEARCH"; |
| |
| /** |
| * Custom action to invoke prepareFromUri() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_PREPARE_FROM_URI = |
| "android.support.v4.media.session.action.PREPARE_FROM_URI"; |
| |
| /** |
| * Custom action to invoke setCaptioningEnabled() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_SET_CAPTIONING_ENABLED = |
| "android.support.v4.media.session.action.SET_CAPTIONING_ENABLED"; |
| |
| /** |
| * Custom action to invoke setRepeatMode() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_SET_REPEAT_MODE = |
| "android.support.v4.media.session.action.SET_REPEAT_MODE"; |
| |
| /** |
| * Custom action to invoke setShuffleMode() for the forward compatibility. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_SET_SHUFFLE_MODE = |
| "android.support.v4.media.session.action.SET_SHUFFLE_MODE"; |
| |
| /** |
| * Custom action to invoke setRating() with extra fields. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_SET_RATING = |
| "android.support.v4.media.session.action.SET_RATING"; |
| |
| /** |
| * Custom action to invoke setPlaybackSpeed() with extra fields. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_SET_PLAYBACK_SPEED = |
| "android.support.v4.media.session.action.SET_PLAYBACK_SPEED"; |
| |
| /** |
| * Argument for use with {@link #ACTION_PREPARE_FROM_MEDIA_ID} indicating media id to play. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_MEDIA_ID = |
| "android.support.v4.media.session.action.ARGUMENT_MEDIA_ID"; |
| |
| /** |
| * Argument for use with {@link #ACTION_PREPARE_FROM_SEARCH} indicating search query. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_QUERY = |
| "android.support.v4.media.session.action.ARGUMENT_QUERY"; |
| |
| /** |
| * Argument for use with {@link #ACTION_PREPARE_FROM_URI} and {@link #ACTION_PLAY_FROM_URI} |
| * indicating URI to play. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_URI = |
| "android.support.v4.media.session.action.ARGUMENT_URI"; |
| |
| /** |
| * Argument for use with {@link #ACTION_SET_RATING} indicating the rate to be set. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_RATING = |
| "android.support.v4.media.session.action.ARGUMENT_RATING"; |
| |
| /** |
| * Argument for use with {@link #ACTION_SET_PLAYBACK_SPEED} indicating the speed to be set. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_PLAYBACK_SPEED = |
| "android.support.v4.media.session.action.ARGUMENT_PLAYBACK_SPEED"; |
| |
| /** |
| * Argument for use with various actions indicating extra bundle. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_EXTRAS = |
| "android.support.v4.media.session.action.ARGUMENT_EXTRAS"; |
| |
| /** |
| * Argument for use with {@link #ACTION_SET_CAPTIONING_ENABLED} indicating whether captioning is |
| * enabled. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_CAPTIONING_ENABLED = |
| "android.support.v4.media.session.action.ARGUMENT_CAPTIONING_ENABLED"; |
| |
| /** |
| * Argument for use with {@link #ACTION_SET_REPEAT_MODE} indicating repeat mode. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_REPEAT_MODE = |
| "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE"; |
| |
| /** |
| * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE} indicating shuffle mode. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String ACTION_ARGUMENT_SHUFFLE_MODE = |
| "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE"; |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String KEY_TOKEN = "android.support.v4.media.session.TOKEN"; |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String KEY_EXTRA_BINDER = |
| "android.support.v4.media.session.EXTRA_BINDER"; |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public static final String KEY_SESSION2_TOKEN = |
| "android.support.v4.media.session.SESSION_TOKEN2"; |
| |
| // Maximum size of the bitmap in dp. |
| private static final int MAX_BITMAP_SIZE_IN_DP = 320; |
| |
| private static final String DATA_CALLING_PACKAGE = "data_calling_pkg"; |
| private static final String DATA_CALLING_PID = "data_calling_pid"; |
| private static final String DATA_CALLING_UID = "data_calling_uid"; |
| private static final String DATA_EXTRAS = "data_extras"; |
| |
| // Maximum size of the bitmap in px. It shouldn't be changed. |
| static int sMaxBitmapSize; |
| |
| /** |
| * Creates a new session. You must call {@link #release()} when finished with the session. |
| * <p> |
| * The session will automatically be registered with the system but will not be published |
| * until {@link #setActive(boolean) setActive(true)} is called. |
| * </p><p> |
| * For API 20 or earlier, note that a media button receiver is required for handling |
| * {@link Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate |
| * {@link BroadcastReceiver} from your manifest. See {@link MediaButtonReceiver} for more |
| * details. |
| * </p> |
| * @param context The context to use to create the session. |
| * @param tag A short name for debugging purposes. |
| */ |
| public MediaSessionCompat(@NonNull Context context, @NonNull String tag) { |
| this(context, tag, null, null); |
| } |
| |
| /** |
| * Creates a new session with a specified media button receiver (a component name and/or |
| * a pending intent). You must call {@link #release()} when finished with the session. |
| * <p> |
| * The session will automatically be registered with the system but will not be published |
| * until {@link #setActive(boolean) setActive(true)} is called. |
| * </p><p> |
| * For API 20 or earlier, note that a media button receiver is required for handling |
| * {@link Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate |
| * {@link BroadcastReceiver} from your manifest if it's not specified. See |
| * {@link MediaButtonReceiver} for more details. |
| * </p> |
| * @param context The context to use to create the session. |
| * @param tag A short name for debugging purposes. |
| * @param mbrComponent The component name for your media button receiver. |
| * @param mbrIntent The PendingIntent for your receiver component that handles |
| * media button events. This is optional and will be used on between |
| * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and |
| * {@link android.os.Build.VERSION_CODES#KITKAT_WATCH} instead of the |
| * component name. |
| */ |
| public MediaSessionCompat(@NonNull Context context, @NonNull String tag, |
| @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent) { |
| this(context, tag, mbrComponent, mbrIntent, null); |
| } |
| |
| /** |
| * Creates a new session with a specified media button receiver (a component name and/or |
| * a pending intent). You must call {@link #release()} when finished with the session. |
| * <p> |
| * The session will automatically be registered with the system but will not be published |
| * until {@link #setActive(boolean) setActive(true)} is called. |
| * </p><p> |
| * For API 20 or earlier, note that a media button receiver is required for handling |
| * {@link Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate |
| * {@link BroadcastReceiver} from your manifest if it's not specified. See |
| * {@link MediaButtonReceiver} for more details. |
| * </p> |
| * The {@code sessionInfo} can include additional unchanging information about this session. |
| * For example, it can include the version of the application, or other app-specific |
| * unchanging information. |
| * |
| * @param context The context to use to create the session. |
| * @param tag A short name for debugging purposes. |
| * @param mbrComponent The component name for your media button receiver. |
| * @param mbrIntent The PendingIntent for your receiver component that handles |
| * media button events. This is optional and will be used on between |
| * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and |
| * {@link android.os.Build.VERSION_CODES#KITKAT_WATCH} instead of the |
| * component name. |
| * @param sessionInfo A bundle for additional information about this session, |
| * or {@link Bundle#EMPTY} if none. Controllers can get this information |
| * by calling {@link MediaControllerCompat#getSessionInfo()}. An |
| * {@link IllegalArgumentException} will be thrown if this contains any |
| * non-framework Parcelable objects. |
| */ |
| public MediaSessionCompat(@NonNull Context context, @NonNull String tag, |
| @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent, |
| @Nullable Bundle sessionInfo) { |
| this(context, tag, mbrComponent, mbrIntent, sessionInfo, null /* session2Token */); |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public MediaSessionCompat(@NonNull Context context, @NonNull String tag, |
| @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent, |
| @Nullable Bundle sessionInfo, @Nullable VersionedParcelable session2Token) { |
| if (context == null) { |
| throw new IllegalArgumentException("context must not be null"); |
| } |
| if (TextUtils.isEmpty(tag)) { |
| throw new IllegalArgumentException("tag must not be null or empty"); |
| } |
| |
| if (mbrComponent == null) { |
| mbrComponent = MediaButtonReceiver.getMediaButtonReceiverComponent(context); |
| if (mbrComponent == null) { |
| Log.w(TAG, "Couldn't find a unique registered media button receiver in the " |
| + "given context."); |
| } |
| } |
| if (mbrComponent != null && mbrIntent == null) { |
| // construct a PendingIntent for the media button |
| Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); |
| // the associated intent will be handled by the component being registered |
| mediaButtonIntent.setComponent(mbrComponent); |
| mbrIntent = PendingIntent.getBroadcast(context, |
| 0/* requestCode, ignored */, mediaButtonIntent, |
| Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0); |
| } |
| |
| if (android.os.Build.VERSION.SDK_INT >= 21) { |
| if (android.os.Build.VERSION.SDK_INT >= 29) { |
| mImpl = new MediaSessionImplApi29(context, tag, session2Token, sessionInfo); |
| } else if (android.os.Build.VERSION.SDK_INT >= 28) { |
| mImpl = new MediaSessionImplApi28(context, tag, session2Token, sessionInfo); |
| } else if (android.os.Build.VERSION.SDK_INT >= 22) { |
| mImpl = new MediaSessionImplApi22(context, tag, session2Token, sessionInfo); |
| } else { |
| mImpl = new MediaSessionImplApi21(context, tag, session2Token, sessionInfo); |
| } |
| // Set default callback to respond to controllers' extra binder requests. |
| Handler handler = new Handler(Looper.myLooper() != null |
| ? Looper.myLooper() : Looper.getMainLooper()); |
| setCallback(new Callback() {}, handler); |
| mImpl.setMediaButtonReceiver(mbrIntent); |
| } else if (android.os.Build.VERSION.SDK_INT >= 19) { |
| mImpl = new MediaSessionImplApi19(context, tag, mbrComponent, mbrIntent, |
| session2Token, sessionInfo); |
| } else if (android.os.Build.VERSION.SDK_INT >= 18) { |
| mImpl = new MediaSessionImplApi18(context, tag, mbrComponent, mbrIntent, |
| session2Token, sessionInfo); |
| } else { |
| mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent, session2Token, |
| sessionInfo); |
| } |
| mController = new MediaControllerCompat(context, this); |
| |
| if (sMaxBitmapSize == 0) { |
| sMaxBitmapSize = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| MAX_BITMAP_SIZE_IN_DP, context.getResources().getDisplayMetrics()) + 0.5f); |
| } |
| } |
| |
| private MediaSessionCompat(Context context, MediaSessionImpl impl) { |
| mImpl = impl; |
| mController = new MediaControllerCompat(context, this); |
| } |
| |
| /** |
| * Adds a callback to receive updates on for the MediaSession. This includes |
| * media button and volume events. The caller's thread will be used to post |
| * events. Set the callback to null to stop receiving events. |
| * <p> |
| * Don't reuse the callback among the sessions. Callbacks keep internal reference to the |
| * session when it's set, so it may misbehave. |
| * |
| * @param callback The callback object |
| */ |
| public void setCallback(Callback callback) { |
| setCallback(callback, null); |
| } |
| |
| /** |
| * Sets the callback to receive updates for the MediaSession. This includes |
| * media button and volume events. Set the callback to null to stop |
| * receiving events. |
| * <p> |
| * Don't reuse the callback among the sessions. Callbacks keep internal reference to the |
| * session when it's set, so it may misbehave. |
| * |
| * @param callback The callback to receive updates on. |
| * @param handler The handler that events should be posted on. |
| */ |
| @SuppressWarnings("deprecation") |
| public void setCallback(Callback callback, Handler handler) { |
| if (callback == null) { |
| mImpl.setCallback(null, null); |
| } else { |
| mImpl.setCallback(callback, handler != null ? handler : new Handler()); |
| } |
| } |
| |
| /** |
| * Sets the {@link RegistrationCallback}. |
| * |
| * @param callback callback to listener callback registration. Can be null to stop. |
| * @param handler handler |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media3-session |
| public void setRegistrationCallback( |
| @Nullable RegistrationCallback callback, @NonNull Handler handler) { |
| mImpl.setRegistrationCallback(callback, handler); |
| } |
| |
| /** |
| * Sets an intent for launching UI for this Session. This can be used as a |
| * quick link to an ongoing media screen. The intent should be for an |
| * activity that may be started using |
| * {@link Activity#startActivity(Intent)}. |
| * |
| * @param pi The intent to launch to show UI for this Session. |
| */ |
| public void setSessionActivity(PendingIntent pi) { |
| mImpl.setSessionActivity(pi); |
| } |
| |
| /** |
| * Sets a pending intent for your media button receiver to allow restarting |
| * playback after the session has been stopped. If your app is started in |
| * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via |
| * the pending intent. |
| * <p> |
| * This method will only work on |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. Earlier |
| * platform versions must include the media button receiver in the |
| * constructor. |
| * |
| * @param mbr The {@link PendingIntent} to send the media button event to. |
| */ |
| public void setMediaButtonReceiver(PendingIntent mbr) { |
| mImpl.setMediaButtonReceiver(mbr); |
| } |
| |
| /** |
| * Sets any flags for the session. |
| * |
| * @param flags The flags to set for this session. |
| */ |
| public void setFlags(@SessionFlags int flags) { |
| mImpl.setFlags(flags); |
| } |
| |
| /** |
| * Sets the stream this session is playing on. This will affect the system's |
| * volume handling for this session. If {@link #setPlaybackToRemote} was |
| * previously called it will stop receiving volume commands and the system |
| * will begin sending volume changes to the appropriate stream. |
| * <p> |
| * By default sessions are on {@link AudioManager#STREAM_MUSIC}. |
| * |
| * @param stream The {@link AudioManager} stream this session is playing on. |
| */ |
| public void setPlaybackToLocal(int stream) { |
| mImpl.setPlaybackToLocal(stream); |
| } |
| |
| /** |
| * Configures this session to use remote volume handling. This must be called |
| * to receive volume button events, otherwise the system will adjust the |
| * current stream volume for this session. If {@link #setPlaybackToLocal} |
| * was previously called that stream will stop receiving volume changes for |
| * this session. |
| * <p> |
| * On platforms earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP} |
| * this will only allow an app to handle volume commands sent directly to |
| * the session by a {@link MediaControllerCompat}. System routing of volume |
| * keys will not use the volume provider. |
| * |
| * @param volumeProvider The provider that will handle volume changes. May |
| * not be null. |
| */ |
| public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { |
| if (volumeProvider == null) { |
| throw new IllegalArgumentException("volumeProvider may not be null!"); |
| } |
| mImpl.setPlaybackToRemote(volumeProvider); |
| } |
| |
| /** |
| * Sets if this session is currently active and ready to receive commands. If |
| * set to false your session's controller may not be discoverable. You must |
| * set the session to active before it can start receiving media button |
| * events or transport commands. |
| * <p> |
| * On platforms earlier than |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, |
| * a media button event receiver should be set via the constructor to |
| * receive media button events. |
| * |
| * @param active Whether this session is active or not. |
| */ |
| public void setActive(boolean active) { |
| mImpl.setActive(active); |
| for (OnActiveChangeListener listener : mActiveListeners) { |
| listener.onActiveChanged(); |
| } |
| } |
| |
| /** |
| * Gets the current active state of this session. |
| * |
| * @return True if the session is active, false otherwise. |
| */ |
| public boolean isActive() { |
| return mImpl.isActive(); |
| } |
| |
| /** |
| * Sends a proprietary event to all MediaControllers listening to this |
| * Session. It's up to the Controller/Session owner to determine the meaning |
| * of any events. |
| * |
| * @param event The name of the event to send |
| * @param extras Any extras included with the event |
| */ |
| public void sendSessionEvent(String event, Bundle extras) { |
| if (TextUtils.isEmpty(event)) { |
| throw new IllegalArgumentException("event cannot be null or empty"); |
| } |
| mImpl.sendSessionEvent(event, extras); |
| } |
| |
| /** |
| * This must be called when an app has finished performing playback. If |
| * playback is expected to start again shortly the session can be left open, |
| * but it must be released if your activity or service is being destroyed. |
| */ |
| public void release() { |
| mImpl.release(); |
| } |
| |
| /** |
| * Retrieves a token object that can be used by apps to create a |
| * {@link MediaControllerCompat} for interacting with this session. The |
| * owner of the session is responsible for deciding how to distribute these |
| * tokens. |
| * <p> |
| * On platform versions before |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP} this token may only be |
| * used within your app as there is no way to guarantee other apps are using |
| * the same version of the support library. |
| * |
| * @return A token that can be used to create a media controller for this |
| * session. |
| */ |
| public Token getSessionToken() { |
| return mImpl.getSessionToken(); |
| } |
| |
| /** |
| * Gets a controller for this session. This is a convenience method to avoid |
| * having to cache your own controller in process. |
| * |
| * @return A controller for this session. |
| */ |
| public MediaControllerCompat getController() { |
| return mController; |
| } |
| |
| /** |
| * Updates the current playback state. |
| * |
| * @param state The current state of playback |
| */ |
| public void setPlaybackState(PlaybackStateCompat state) { |
| mImpl.setPlaybackState(state); |
| } |
| |
| /** |
| * Updates the current metadata. New metadata can be created using |
| * {@link android.support.v4.media.MediaMetadataCompat.Builder}. This operation may take time |
| * proportional to the size of the bitmap to replace large bitmaps with a scaled down copy. |
| * |
| * @param metadata The new metadata |
| * @see android.support.v4.media.MediaMetadataCompat.Builder#putBitmap |
| */ |
| public void setMetadata(MediaMetadataCompat metadata) { |
| mImpl.setMetadata(metadata); |
| } |
| |
| /** |
| * Updates the list of items in the play queue. It is an ordered list and |
| * should contain the current item, and previous or upcoming items if they |
| * exist. The id of each item should be unique within the play queue. |
| * Specify null if there is no current play queue. |
| * <p> |
| * The queue should be of reasonable size. If the play queue is unbounded |
| * within your app, it is better to send a reasonable amount in a sliding |
| * window instead. |
| * |
| * @param queue A list of items in the play queue. |
| */ |
| public void setQueue(List<QueueItem> queue) { |
| if (queue != null) { |
| Set<Long> set = new HashSet<>(); |
| for (QueueItem item : queue) { |
| if (item == null) { |
| throw new IllegalArgumentException("queue shouldn't have null items"); |
| } |
| if (set.contains(item.getQueueId())) { |
| Log.e(TAG, "Found duplicate queue id: " + item.getQueueId(), |
| new IllegalArgumentException("id of each queue item should be unique")); |
| } |
| set.add(item.getQueueId()); |
| } |
| } |
| mImpl.setQueue(queue); |
| } |
| |
| /** |
| * Sets the title of the play queue. The UI should display this title along |
| * with the play queue itself. e.g. "Play Queue", "Now Playing", or an album |
| * name. |
| * |
| * @param title The title of the play queue. |
| */ |
| public void setQueueTitle(CharSequence title) { |
| mImpl.setQueueTitle(title); |
| } |
| |
| /** |
| * Sets the style of rating used by this session. Apps trying to set the |
| * rating should use this style. Must be one of the following: |
| * <ul> |
| * <li>{@link RatingCompat#RATING_NONE}</li> |
| * <li>{@link RatingCompat#RATING_3_STARS}</li> |
| * <li>{@link RatingCompat#RATING_4_STARS}</li> |
| * <li>{@link RatingCompat#RATING_5_STARS}</li> |
| * <li>{@link RatingCompat#RATING_HEART}</li> |
| * <li>{@link RatingCompat#RATING_PERCENTAGE}</li> |
| * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li> |
| * </ul> |
| */ |
| public void setRatingType(@RatingCompat.Style int type) { |
| mImpl.setRatingType(type); |
| } |
| |
| /** |
| * Enables/disables captioning for this session. |
| * |
| * @param enabled {@code true} to enable captioning, {@code false} to disable. |
| */ |
| public void setCaptioningEnabled(boolean enabled) { |
| mImpl.setCaptioningEnabled(enabled); |
| } |
| |
| /** |
| * Sets the repeat mode for this session. |
| * <p> |
| * Note that if this method is not called before, {@link MediaControllerCompat#getRepeatMode} |
| * will return {@link PlaybackStateCompat#REPEAT_MODE_NONE}. |
| * |
| * @param repeatMode The repeat mode. Must be one of the followings: |
| * {@link PlaybackStateCompat#REPEAT_MODE_NONE}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_ONE}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_ALL}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_GROUP} |
| */ |
| public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { |
| mImpl.setRepeatMode(repeatMode); |
| } |
| |
| /** |
| * Sets the shuffle mode for this session. |
| * <p> |
| * Note that if this method is not called before, {@link MediaControllerCompat#getShuffleMode} |
| * will return {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}. |
| * |
| * @param shuffleMode The shuffle mode. Must be one of the followings: |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}, |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} |
| */ |
| public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { |
| mImpl.setShuffleMode(shuffleMode); |
| } |
| |
| /** |
| * Sets some extras that can be associated with the |
| * {@link MediaSessionCompat}. No assumptions should be made as to how a |
| * {@link MediaControllerCompat} will handle these extras. Keys should be |
| * fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. |
| * |
| * @param extras The extras associated with the session. |
| */ |
| public void setExtras(Bundle extras) { |
| mImpl.setExtras(extras); |
| } |
| |
| /** |
| * Gets the underlying framework {@link android.media.session.MediaSession} |
| * object. |
| * <p> |
| * This method is only supported on API 21+. |
| * </p> |
| * |
| * @return The underlying {@link android.media.session.MediaSession} object, |
| * or null if none. |
| */ |
| public Object getMediaSession() { |
| return mImpl.getMediaSession(); |
| } |
| |
| /** |
| * Gets the underlying framework {@link android.media.RemoteControlClient} |
| * object. |
| * <p> |
| * This method is only supported on APIs 14-20. On API 21+ |
| * {@link #getMediaSession()} should be used instead. |
| * |
| * @return The underlying {@link android.media.RemoteControlClient} object, |
| * or null if none. |
| */ |
| public Object getRemoteControlClient() { |
| return mImpl.getRemoteControlClient(); |
| } |
| |
| /** |
| * Gets the controller information who sent the current request. |
| * <p> |
| * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}. |
| * <p> |
| * Note: From API 21 to 23, this method returns a fake {@link RemoteUserInfo} which has |
| * following values: |
| * <ul> |
| * <li>Package name is {@link MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER}.</li> |
| * <li>PID and UID will have negative values.</li> |
| * </ul> |
| * <p> |
| * Note: From API 24 to 27, the {@link RemoteUserInfo} returned from this method will have |
| * negative uid and pid. Most of the cases it will have the correct package name, but sometimes |
| * it will fail to get the right one. |
| * |
| * @see MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER |
| * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) |
| */ |
| @NonNull |
| public final RemoteUserInfo getCurrentControllerInfo() { |
| return mImpl.getCurrentControllerInfo(); |
| } |
| |
| /** |
| * Returns the name of the package that sent the last media button, transport control, or |
| * command from controllers and the system. This is only valid while in a request callback, such |
| * as {@link Callback#onPlay}. This method is not available and returns null on pre-N devices. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public String getCallingPackage() { |
| return mImpl.getCallingPackage(); |
| } |
| |
| /** |
| * Adds a listener to be notified when the active status of this session |
| * changes. This is primarily used by the support library and should not be |
| * needed by apps. |
| * |
| * @param listener The listener to add. |
| */ |
| public void addOnActiveChangeListener(OnActiveChangeListener listener) { |
| if (listener == null) { |
| throw new IllegalArgumentException("Listener may not be null"); |
| } |
| mActiveListeners.add(listener); |
| } |
| |
| /** |
| * Stops the listener from being notified when the active status of this |
| * session changes. |
| * |
| * @param listener The listener to remove. |
| */ |
| public void removeOnActiveChangeListener(OnActiveChangeListener listener) { |
| if (listener == null) { |
| throw new IllegalArgumentException("Listener may not be null"); |
| } |
| mActiveListeners.remove(listener); |
| } |
| |
| /** |
| * Creates an instance from a framework {@link android.media.session.MediaSession} object. |
| * <p> |
| * This method is only supported on API 21+. On API 20 and below, it returns null. |
| * <p> |
| * Note: A {@link MediaSessionCompat} object returned from this method may not provide the full |
| * functionality of {@link MediaSessionCompat} until setting a new |
| * {@link MediaSessionCompat.Callback}. To avoid this, when both a {@link MediaSessionCompat} |
| * and a framework {@link android.media.session.MediaSession} are needed, it is recommended |
| * to create a {@link MediaSessionCompat} first and get the framework session through |
| * {@link #getMediaSession()}. |
| * |
| * @param context The context to use to create the session. |
| * @param mediaSession A {@link android.media.session.MediaSession} object. |
| * @return An equivalent {@link MediaSessionCompat} object, or null if none. |
| */ |
| public static MediaSessionCompat fromMediaSession(Context context, Object mediaSession) { |
| if (Build.VERSION.SDK_INT < 21 || context == null || mediaSession == null) { |
| return null; |
| } |
| MediaSessionImpl impl; |
| if (Build.VERSION.SDK_INT >= 29) { |
| impl = new MediaSessionImplApi29(mediaSession); |
| } else if (Build.VERSION.SDK_INT >= 28) { |
| impl = new MediaSessionImplApi28(mediaSession); |
| } else { |
| // API 21+ |
| impl = new MediaSessionImplApi21(mediaSession); |
| } |
| return new MediaSessionCompat(context, impl); |
| } |
| |
| /** |
| * A helper method for setting the application class loader to the given {@link Bundle}. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| public static void ensureClassLoader(@Nullable Bundle bundle) { |
| if (bundle != null) { |
| bundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); |
| } |
| } |
| |
| /** |
| * Tries to unparcel the given {@link Bundle} with the application class loader and |
| * returns {@code null} if a {@link BadParcelableException} is thrown while unparcelling, |
| * otherwise the given bundle in which the application class loader is set. |
| * |
| */ |
| @RestrictTo(LIBRARY) |
| @Nullable |
| public static Bundle unparcelWithClassLoader(@Nullable Bundle bundle) { |
| if (bundle == null) { |
| return null; |
| } |
| ensureClassLoader(bundle); |
| try { |
| bundle.isEmpty(); // to call unparcel() |
| return bundle; |
| } catch (BadParcelableException e) { |
| // The exception details will be logged by Parcel class. |
| Log.e(TAG, "Could not unparcel the data."); |
| return null; |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static PlaybackStateCompat getStateWithUpdatedPosition( |
| PlaybackStateCompat state, MediaMetadataCompat metadata) { |
| if (state == null || state.getPosition() == PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN) { |
| return state; |
| } |
| |
| if (state.getState() == PlaybackStateCompat.STATE_PLAYING |
| || state.getState() == PlaybackStateCompat.STATE_FAST_FORWARDING |
| || state.getState() == PlaybackStateCompat.STATE_REWINDING) { |
| long updateTime = state.getLastPositionUpdateTime(); |
| if (updateTime > 0) { |
| long currentTime = SystemClock.elapsedRealtime(); |
| long position = (long) (state.getPlaybackSpeed() * (currentTime - updateTime)) |
| + state.getPosition(); |
| long duration = -1; |
| if (metadata != null && metadata.containsKey( |
| MediaMetadataCompat.METADATA_KEY_DURATION)) { |
| duration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); |
| } |
| |
| if (duration >= 0 && position > duration) { |
| position = duration; |
| } else if (position < 0) { |
| position = 0; |
| } |
| return new PlaybackStateCompat.Builder(state) |
| .setState(state.getState(), position, state.getPlaybackSpeed(), currentTime) |
| .build(); |
| } |
| } |
| return state; |
| } |
| |
| /** |
| * Receives transport controls, media buttons, and commands from controllers |
| * and the system. The callback may be set using {@link #setCallback}. |
| * <p> |
| * Don't reuse the callback among the sessions. Callbacks keep internal reference to the |
| * session when it's set, so it may misbehave. |
| */ |
| public abstract static class Callback { |
| final Object mLock = new Object(); |
| final MediaSession.Callback mCallbackFwk; |
| private boolean mMediaPlayPausePendingOnHandler; |
| |
| @GuardedBy("mLock") |
| WeakReference<MediaSessionImpl> mSessionImpl; |
| @GuardedBy("mLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| CallbackHandler mCallbackHandler; |
| |
| public Callback() { |
| if (android.os.Build.VERSION.SDK_INT >= 21) { |
| mCallbackFwk = new MediaSessionCallbackApi21(); |
| } else { |
| mCallbackFwk = null; |
| } |
| mSessionImpl = new WeakReference<>(null); |
| } |
| |
| void setSessionImpl(MediaSessionImpl impl, Handler handler) { |
| synchronized (mLock) { |
| mSessionImpl = new WeakReference<MediaSessionImpl>(impl); |
| if (mCallbackHandler != null) { |
| mCallbackHandler.removeCallbacksAndMessages(null); |
| } |
| mCallbackHandler = impl == null || handler == null ? null : |
| new CallbackHandler(handler.getLooper()); |
| } |
| } |
| |
| /** |
| * Called when a controller has sent a custom command to this session. |
| * The owner of the session may handle custom commands but is not |
| * required to. |
| * |
| * @param command The command name. |
| * @param extras Optional parameters for the command, may be null. |
| * @param cb A result receiver to which a result may be sent by the command, may be null. |
| */ |
| public void onCommand(String command, Bundle extras, ResultReceiver cb) { |
| } |
| |
| /** |
| * Override to handle media button events. |
| * <p> |
| * The double tap of {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} or {@link |
| * KeyEvent#KEYCODE_HEADSETHOOK} will call the {@link #onSkipToNext} by default. If the |
| * current SDK level is 27 or higher, the default double tap handling is done by framework |
| * so this method would do nothing for it. |
| * |
| * @param mediaButtonEvent The media button event intent. |
| * @return True if the event was handled, false otherwise. |
| */ |
| @SuppressWarnings("deprecation") |
| public boolean onMediaButtonEvent(Intent mediaButtonEvent) { |
| if (android.os.Build.VERSION.SDK_INT >= 27) { |
| // Double tap of play/pause as skipping to next is already handled by framework, |
| // so we don't need to repeat again here. |
| // Note: Double tap would be handled twice for OC-DR1 whose SDK version 26 and |
| // framework handles the double tap. |
| return false; |
| } |
| MediaSessionImpl impl; |
| Handler callbackHandler; |
| synchronized (mLock) { |
| impl = mSessionImpl.get(); |
| callbackHandler = mCallbackHandler; |
| } |
| if (impl == null || callbackHandler == null) { |
| return false; |
| } |
| KeyEvent keyEvent = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); |
| if (keyEvent == null || keyEvent.getAction() != KeyEvent.ACTION_DOWN) { |
| return false; |
| } |
| RemoteUserInfo remoteUserInfo = impl.getCurrentControllerInfo(); |
| int keyCode = keyEvent.getKeyCode(); |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: |
| case KeyEvent.KEYCODE_HEADSETHOOK: |
| if (keyEvent.getRepeatCount() == 0) { |
| if (mMediaPlayPausePendingOnHandler) { |
| callbackHandler.removeMessages( |
| CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); |
| mMediaPlayPausePendingOnHandler = false; |
| PlaybackStateCompat state = impl.getPlaybackState(); |
| long validActions = state == null ? 0 : state.getActions(); |
| // Consider double tap as the next. |
| if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { |
| onSkipToNext(); |
| } |
| } else { |
| mMediaPlayPausePendingOnHandler = true; |
| callbackHandler.sendMessageDelayed(callbackHandler.obtainMessage( |
| CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT, |
| remoteUserInfo), |
| ViewConfiguration.getDoubleTapTimeout()); |
| } |
| } else { |
| // Consider long-press as a single tap. |
| handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); |
| } |
| return true; |
| default: |
| // If another key is pressed within double tap timeout, consider the pending |
| // pending play/pause as a single tap to handle media keys in order. |
| handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); |
| break; |
| } |
| return false; |
| } |
| |
| void handleMediaPlayPauseIfPendingOnHandler(MediaSessionImpl impl, |
| Handler callbackHandler) { |
| if (!mMediaPlayPausePendingOnHandler) { |
| return; |
| } |
| mMediaPlayPausePendingOnHandler = false; |
| callbackHandler.removeMessages( |
| CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); |
| PlaybackStateCompat state = impl.getPlaybackState(); |
| long validActions = state == null ? 0 : state.getActions(); |
| boolean isPlaying = state != null |
| && state.getState() == PlaybackStateCompat.STATE_PLAYING; |
| boolean canPlay = (validActions & (PlaybackStateCompat.ACTION_PLAY_PAUSE |
| | PlaybackStateCompat.ACTION_PLAY)) != 0; |
| boolean canPause = (validActions & (PlaybackStateCompat.ACTION_PLAY_PAUSE |
| | PlaybackStateCompat.ACTION_PAUSE)) != 0; |
| if (isPlaying && canPause) { |
| onPause(); |
| } else if (!isPlaying && canPlay) { |
| onPlay(); |
| } |
| } |
| |
| /** |
| * Override to handle requests to prepare playback. Override {@link #onPlay} to handle |
| * requests for starting playback. |
| */ |
| public void onPrepare() { |
| } |
| |
| /** |
| * Override to handle requests to prepare for playing a specific mediaId that was provided |
| * by your app. Override {@link #onPlayFromMediaId} to handle requests for starting |
| * playback. |
| */ |
| public void onPrepareFromMediaId(String mediaId, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to prepare playback from a search query. An |
| * empty query indicates that the app may prepare any music. The |
| * implementation should attempt to make a smart choice about what to play. |
| * Override {@link #onPlayFromSearch} to handle requests |
| * for starting playback. |
| */ |
| public void onPrepareFromSearch(String query, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to prepare a specific media item represented by a URI. |
| * Override {@link #onPlayFromUri} to handle requests |
| * for starting playback. |
| */ |
| public void onPrepareFromUri(Uri uri, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to begin playback. |
| */ |
| public void onPlay() { |
| } |
| |
| /** |
| * Override to handle requests to play a specific mediaId that was |
| * provided by your app. |
| */ |
| public void onPlayFromMediaId(String mediaId, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to begin playback from a search query. An |
| * empty query indicates that the app may play any music. The |
| * implementation should attempt to make a smart choice about what to |
| * play. |
| */ |
| public void onPlayFromSearch(String query, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to play a specific media item represented by a URI. |
| */ |
| public void onPlayFromUri(Uri uri, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle requests to play an item with a given id from the |
| * play queue. |
| */ |
| public void onSkipToQueueItem(long id) { |
| } |
| |
| /** |
| * Override to handle requests to pause playback. |
| */ |
| public void onPause() { |
| } |
| |
| /** |
| * Override to handle requests to skip to the next media item. |
| */ |
| public void onSkipToNext() { |
| } |
| |
| /** |
| * Override to handle requests to skip to the previous media item. |
| */ |
| public void onSkipToPrevious() { |
| } |
| |
| /** |
| * Override to handle requests to fast forward. |
| */ |
| public void onFastForward() { |
| } |
| |
| /** |
| * Override to handle requests to rewind. |
| */ |
| public void onRewind() { |
| } |
| |
| /** |
| * Override to handle requests to stop playback. |
| */ |
| public void onStop() { |
| } |
| |
| /** |
| * Override to handle requests to seek to a specific position in ms. |
| * |
| * @param pos New position to move to, in milliseconds. |
| */ |
| public void onSeekTo(long pos) { |
| } |
| |
| /** |
| * Override to handle the item being rated. |
| * |
| * @param rating The rating being set. |
| */ |
| public void onSetRating(RatingCompat rating) { |
| } |
| |
| /** |
| * Override to handle the item being rated. |
| * |
| * @param rating The rating being set. |
| * @param extras The extras can include information about the media item being rated. |
| */ |
| public void onSetRating(RatingCompat rating, Bundle extras) { |
| } |
| |
| /** |
| * Override to handle the playback speed change. |
| * To update the new playback speed, create a new {@link PlaybackStateCompat} by using |
| * {@link PlaybackStateCompat.Builder#setState(int, long, float)}, and set it with |
| * {@link #setPlaybackState(PlaybackStateCompat)}. |
| * <p> |
| * A value of {@code 1.0f} is the default playback value, and a negative value indicates |
| * reverse playback. The {@code speed} will not be equal to zero. |
| * |
| * @param speed the playback speed |
| * @see #setPlaybackState(PlaybackStateCompat) |
| * @see PlaybackStateCompat.Builder#setState(int, long, float) |
| */ |
| public void onSetPlaybackSpeed(float speed) { |
| } |
| |
| /** |
| * Override to handle requests to enable/disable captioning. |
| * |
| * @param enabled {@code true} to enable captioning, {@code false} to disable. |
| */ |
| public void onSetCaptioningEnabled(boolean enabled) { |
| } |
| |
| /** |
| * Override to handle the setting of the repeat mode. |
| * <p> |
| * You should call {@link #setRepeatMode} before end of this method in order to notify |
| * the change to the {@link MediaControllerCompat}, or |
| * {@link MediaControllerCompat#getRepeatMode} could return an invalid value. |
| * |
| * @param repeatMode The repeat mode which is one of followings: |
| * {@link PlaybackStateCompat#REPEAT_MODE_NONE}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_ONE}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_ALL}, |
| * {@link PlaybackStateCompat#REPEAT_MODE_GROUP} |
| */ |
| public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { |
| } |
| |
| /** |
| * Override to handle the setting of the shuffle mode. |
| * <p> |
| * You should call {@link #setShuffleMode} before the end of this method in order to |
| * notify the change to the {@link MediaControllerCompat}, or |
| * {@link MediaControllerCompat#getShuffleMode} could return an invalid value. |
| * |
| * @param shuffleMode The shuffle mode which is one of followings: |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}, |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, |
| * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} |
| */ |
| public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { |
| } |
| |
| /** |
| * Called when a {@link MediaControllerCompat} wants a |
| * {@link PlaybackStateCompat.CustomAction} to be performed. |
| * |
| * @param action The action that was originally sent in the |
| * {@link PlaybackStateCompat.CustomAction}. |
| * @param extras Optional extras specified by the |
| * {@link MediaControllerCompat}. |
| * @see #ACTION_FLAG_AS_INAPPROPRIATE |
| * @see #ACTION_SKIP_AD |
| * @see #ACTION_FOLLOW |
| * @see #ACTION_UNFOLLOW |
| */ |
| public void onCustomAction(String action, Bundle extras) { |
| } |
| |
| /** |
| * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem} |
| * with the given {@link MediaDescriptionCompat description} at the end of the play queue. |
| * |
| * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem} |
| * to be inserted. |
| */ |
| public void onAddQueueItem(MediaDescriptionCompat description) { |
| } |
| |
| /** |
| * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem} |
| * with the given {@link MediaDescriptionCompat description} at the specified position |
| * in the play queue. |
| * |
| * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem} |
| * to be inserted. |
| * @param index The index at which the created {@link QueueItem} is to be inserted. |
| */ |
| public void onAddQueueItem(MediaDescriptionCompat description, int index) { |
| } |
| |
| /** |
| * Called when a {@link MediaControllerCompat} wants to remove the first occurrence of the |
| * specified {@link QueueItem} with the given {@link MediaDescriptionCompat description} |
| * in the play queue. |
| * |
| * @param description The {@link MediaDescriptionCompat} for denoting the {@link QueueItem} |
| * to be removed. |
| */ |
| public void onRemoveQueueItem(MediaDescriptionCompat description) { |
| } |
| |
| /** |
| * Called when a {@link MediaControllerCompat} wants to remove a {@link QueueItem} at the |
| * specified position in the play queue. |
| * |
| * @param index The index of the element to be removed. |
| * @deprecated {@link #onRemoveQueueItem} will be called instead. |
| */ |
| @Deprecated |
| public void onRemoveQueueItemAt(int index) { |
| } |
| |
| private class CallbackHandler extends Handler { |
| private static final int MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT = 1; |
| |
| CallbackHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| if (msg.what == MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT) { |
| // Here we manually set the caller info, since this is not directly called from |
| // the session callback. This is triggered by timeout. |
| MediaSessionImpl impl; |
| Handler callbackHandler; |
| synchronized (mLock) { |
| impl = mSessionImpl.get(); |
| callbackHandler = mCallbackHandler; |
| } |
| if (impl == null |
| || MediaSessionCompat.Callback.this != impl.getCallback() |
| || callbackHandler == null) { |
| return; |
| } |
| RemoteUserInfo info = (RemoteUserInfo) msg.obj; |
| impl.setCurrentControllerInfo(info); |
| handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); |
| impl.setCurrentControllerInfo(null); |
| } |
| } |
| } |
| |
| @RequiresApi(21) |
| private class MediaSessionCallbackApi21 extends MediaSession.Callback { |
| MediaSessionCallbackApi21() { |
| } |
| |
| @Override |
| @SuppressWarnings("deprecation") |
| public void onCommand(String command, Bundle extras, ResultReceiver cb) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| try { |
| if (command.equals(MediaControllerCompat.COMMAND_GET_EXTRA_BINDER)) { |
| Bundle result = new Bundle(); |
| Token token = sessionImpl.getSessionToken(); |
| IMediaSession extraBinder = token.getExtraBinder(); |
| BundleCompat.putBinder(result, KEY_EXTRA_BINDER, |
| extraBinder == null ? null : extraBinder.asBinder()); |
| ParcelUtils.putVersionedParcelable(result, |
| KEY_SESSION2_TOKEN, token.getSession2Token()); |
| cb.send(0, result); |
| } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM)) { |
| Callback.this.onAddQueueItem( |
| (MediaDescriptionCompat) extras.getParcelable( |
| MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION)); |
| } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM_AT)) { |
| Callback.this.onAddQueueItem( |
| (MediaDescriptionCompat) extras.getParcelable( |
| MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION), |
| extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX)); |
| } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM)) { |
| Callback.this.onRemoveQueueItem( |
| (MediaDescriptionCompat) extras.getParcelable( |
| MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION)); |
| } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM_AT)) { |
| if (sessionImpl.mQueue != null) { |
| int index = |
| extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX, -1); |
| QueueItem item = (index >= 0 && index < sessionImpl.mQueue.size()) |
| ? sessionImpl.mQueue.get(index) : null; |
| if (item != null) { |
| Callback.this.onRemoveQueueItem(item.getDescription()); |
| } |
| } |
| } else { |
| Callback.this.onCommand(command, extras, cb); |
| } |
| } catch (BadParcelableException e) { |
| // Do not print the exception here, since it is already done by the Parcel |
| // class. |
| Log.e(TAG, "Could not unparcel the extra data."); |
| } |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public boolean onMediaButtonEvent(Intent mediaButtonIntent) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return false; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| boolean result = Callback.this.onMediaButtonEvent(mediaButtonIntent); |
| clearCurrentControllerInfo(sessionImpl); |
| return result || super.onMediaButtonEvent(mediaButtonIntent); |
| } |
| |
| @Override |
| public void onPlay() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPlay(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onPlayFromMediaId(String mediaId, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPlayFromMediaId(mediaId, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onPlayFromSearch(String search, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPlayFromSearch(search, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(23) |
| @Override |
| public void onPlayFromUri(Uri uri, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPlayFromUri(uri, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onSkipToQueueItem(long id) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSkipToQueueItem(id); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onPause() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPause(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onSkipToNext() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSkipToNext(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onSkipToPrevious() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSkipToPrevious(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onFastForward() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onFastForward(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onRewind() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onRewind(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onStop() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onStop(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onSeekTo(long pos) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSeekTo(pos); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @Override |
| public void onSetRating(Rating ratingFwk) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSetRating(RatingCompat.fromRating(ratingFwk)); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| @Override |
| @SuppressWarnings("deprecation") |
| public void onCustomAction(String action, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| |
| try { |
| if (action.equals(ACTION_PLAY_FROM_URI)) { |
| Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); |
| Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); |
| ensureClassLoader(bundle); |
| Callback.this.onPlayFromUri(uri, bundle); |
| } else if (action.equals(ACTION_PREPARE)) { |
| Callback.this.onPrepare(); |
| } else if (action.equals(ACTION_PREPARE_FROM_MEDIA_ID)) { |
| String mediaId = extras.getString(ACTION_ARGUMENT_MEDIA_ID); |
| Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); |
| ensureClassLoader(bundle); |
| Callback.this.onPrepareFromMediaId(mediaId, bundle); |
| } else if (action.equals(ACTION_PREPARE_FROM_SEARCH)) { |
| String query = extras.getString(ACTION_ARGUMENT_QUERY); |
| Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); |
| ensureClassLoader(bundle); |
| Callback.this.onPrepareFromSearch(query, bundle); |
| } else if (action.equals(ACTION_PREPARE_FROM_URI)) { |
| Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); |
| Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); |
| ensureClassLoader(bundle); |
| Callback.this.onPrepareFromUri(uri, bundle); |
| } else if (action.equals(ACTION_SET_CAPTIONING_ENABLED)) { |
| boolean enabled = extras.getBoolean(ACTION_ARGUMENT_CAPTIONING_ENABLED); |
| Callback.this.onSetCaptioningEnabled(enabled); |
| } else if (action.equals(ACTION_SET_REPEAT_MODE)) { |
| int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE); |
| Callback.this.onSetRepeatMode(repeatMode); |
| } else if (action.equals(ACTION_SET_SHUFFLE_MODE)) { |
| int shuffleMode = extras.getInt(ACTION_ARGUMENT_SHUFFLE_MODE); |
| Callback.this.onSetShuffleMode(shuffleMode); |
| } else if (action.equals(ACTION_SET_RATING)) { |
| RatingCompat rating = extras.getParcelable(ACTION_ARGUMENT_RATING); |
| Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); |
| ensureClassLoader(bundle); |
| Callback.this.onSetRating(rating, bundle); |
| } else if (action.equals(ACTION_SET_PLAYBACK_SPEED)) { |
| float speed = extras.getFloat(ACTION_ARGUMENT_PLAYBACK_SPEED, 1.0f); |
| Callback.this.onSetPlaybackSpeed(speed); |
| } else { |
| Callback.this.onCustomAction(action, extras); |
| } |
| } catch (BadParcelableException e) { |
| // The exception details will be logged by Parcel class. |
| Log.e(TAG, "Could not unparcel the data."); |
| } |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(24) |
| @Override |
| public void onPrepare() { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPrepare(); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(24) |
| @Override |
| public void onPrepareFromMediaId(String mediaId, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPrepareFromMediaId(mediaId, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(24) |
| @Override |
| public void onPrepareFromSearch(String query, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPrepareFromSearch(query, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(24) |
| @Override |
| public void onPrepareFromUri(Uri uri, Bundle extras) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| ensureClassLoader(extras); |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onPrepareFromUri(uri, extras); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| @RequiresApi(29) |
| @Override |
| public void onSetPlaybackSpeed(float speed) { |
| MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); |
| if (sessionImpl == null) { |
| return; |
| } |
| setCurrentControllerInfo(sessionImpl); |
| Callback.this.onSetPlaybackSpeed(speed); |
| clearCurrentControllerInfo(sessionImpl); |
| } |
| |
| private void setCurrentControllerInfo(MediaSessionImpl sessionImpl) { |
| if (Build.VERSION.SDK_INT >= 28) { |
| // From API 28, this method has no effect since |
| // MediaSessionImplApi28#getCurrentControllerInfo() returns controller info from |
| // framework. |
| return; |
| } |
| String packageName = sessionImpl.getCallingPackage(); |
| if (TextUtils.isEmpty(packageName)) { |
| packageName = LEGACY_CONTROLLER; |
| } |
| sessionImpl.setCurrentControllerInfo(new RemoteUserInfo( |
| packageName, UNKNOWN_PID, UNKNOWN_UID)); |
| } |
| |
| private void clearCurrentControllerInfo(MediaSessionImpl sessionImpl) { |
| sessionImpl.setCurrentControllerInfo(null); |
| } |
| |
| // Returns the MediaSessionImplApi21 if this callback is still set by the session. |
| // This prevent callback methods to be called after session is release() or |
| // callback is changed. |
| private MediaSessionImplApi21 getSessionImplIfCallbackIsSet() { |
| MediaSessionImplApi21 sessionImpl; |
| synchronized (mLock) { |
| sessionImpl = (MediaSessionImplApi21) mSessionImpl.get(); |
| } |
| return sessionImpl != null |
| && MediaSessionCompat.Callback.this == sessionImpl.getCallback() |
| ? sessionImpl : null; |
| } |
| } |
| } |
| |
| /** |
| * Callback to be called when a controller has registered or unregistered controller callback. |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public interface RegistrationCallback { |
| /** |
| * Called when a {@link MediaControllerCompat} registered callback. |
| * |
| * @param callingPid PID from Binder#getCallingPid() |
| * @param callingUid UID from Binder#getCallingUid() |
| */ |
| void onCallbackRegistered(int callingPid, int callingUid); |
| |
| /** |
| * Called when a {@link MediaControllerCompat} unregistered callback. |
| * |
| * @param callingPid PID from Binder#getCallingPid() |
| * @param callingUid UID from Binder#getCallingUid() |
| */ |
| void onCallbackUnregistered(int callingPid, int callingUid); |
| } |
| |
| /** |
| * Represents an ongoing session. This may be passed to apps by the session |
| * owner to allow them to create a {@link MediaControllerCompat} to communicate with |
| * the session. |
| */ |
| @SuppressLint("BanParcelableUsage") |
| public static final class Token implements Parcelable { |
| private final Object mLock = new Object(); |
| private final Object mInner; |
| |
| @GuardedBy("mLock") |
| private IMediaSession mExtraBinder; |
| @GuardedBy("mLock") |
| private VersionedParcelable mSession2Token; |
| |
| Token(Object inner) { |
| this(inner, null, null); |
| } |
| |
| Token(Object inner, IMediaSession extraBinder) { |
| this(inner, extraBinder, null); |
| } |
| |
| Token(Object inner, IMediaSession extraBinder, VersionedParcelable session2Token) { |
| mInner = inner; |
| mExtraBinder = extraBinder; |
| mSession2Token = session2Token; |
| } |
| |
| /** |
| * Creates a compat Token from a framework |
| * {@link android.media.session.MediaSession.Token} object. |
| * <p> |
| * This method is only supported on |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. |
| * </p> |
| * |
| * @param token The framework token object. |
| * @return A compat Token for use with {@link MediaControllerCompat}. |
| */ |
| public static Token fromToken(Object token) { |
| return fromToken(token, null); |
| } |
| |
| /** |
| * Creates a compat Token from a framework |
| * {@link android.media.session.MediaSession.Token} object, and the extra binder. |
| * <p> |
| * This method is only supported on |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. |
| * </p> |
| * |
| * @param token The framework token object. |
| * @param extraBinder The extra binder. |
| * @return A compat Token for use with {@link MediaControllerCompat}. |
| */ |
| @RestrictTo(LIBRARY) |
| public static Token fromToken(Object token, IMediaSession extraBinder) { |
| if (token != null && android.os.Build.VERSION.SDK_INT >= 21) { |
| if (!(token instanceof MediaSession.Token)) { |
| throw new IllegalArgumentException( |
| "token is not a valid MediaSession.Token object"); |
| } |
| return new Token(token, extraBinder); |
| } |
| return null; |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| if (android.os.Build.VERSION.SDK_INT >= 21) { |
| dest.writeParcelable((Parcelable) mInner, flags); |
| } else { |
| dest.writeStrongBinder((IBinder) mInner); |
| } |
| } |
| |
| @Override |
| public int hashCode() { |
| if (mInner == null) { |
| return 0; |
| } |
| return mInner.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof Token)) { |
| return false; |
| } |
| |
| Token other = (Token) obj; |
| if (mInner == null) { |
| return other.mInner == null; |
| } |
| if (other.mInner == null) { |
| return false; |
| } |
| return mInner.equals(other.mInner); |
| } |
| |
| /** |
| * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. |
| * <p> |
| * This method is only supported on API 21+. |
| * </p> |
| * |
| * @return The underlying {@link android.media.session.MediaSession.Token} object, |
| * or null if none. |
| */ |
| public Object getToken() { |
| return mInner; |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public IMediaSession getExtraBinder() { |
| synchronized (mLock) { |
| return mExtraBinder; |
| } |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY) |
| public void setExtraBinder(IMediaSession extraBinder) { |
| synchronized (mLock) { |
| mExtraBinder = extraBinder; |
| } |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public VersionedParcelable getSession2Token() { |
| synchronized (mLock) { |
| return mSession2Token; |
| } |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public void setSession2Token(VersionedParcelable session2Token) { |
| synchronized (mLock) { |
| mSession2Token = session2Token; |
| } |
| } |
| |
| /** |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| public Bundle toBundle() { |
| Bundle bundle = new Bundle(); |
| bundle.putParcelable(KEY_TOKEN, this); |
| synchronized (mLock) { |
| if (mExtraBinder != null) { |
| BundleCompat.putBinder(bundle, KEY_EXTRA_BINDER, mExtraBinder.asBinder()); |
| } |
| if (mSession2Token != null) { |
| ParcelUtils.putVersionedParcelable(bundle, KEY_SESSION2_TOKEN, mSession2Token); |
| } |
| } |
| return bundle; |
| } |
| |
| /** |
| * Creates a compat Token from a bundle object. |
| * |
| * @param tokenBundle |
| * @return A compat Token for use with {@link MediaControllerCompat}. |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) // accessed by media2-session |
| @SuppressWarnings("deprecation") |
| public static Token fromBundle(Bundle tokenBundle) { |
| if (tokenBundle == null) { |
| return null; |
| } |
| tokenBundle.setClassLoader(Token.class.getClassLoader()); |
| IMediaSession extraSession = IMediaSession.Stub.asInterface( |
| BundleCompat.getBinder(tokenBundle, KEY_EXTRA_BINDER)); |
| VersionedParcelable session2Token = ParcelUtils.getVersionedParcelable(tokenBundle, |
| KEY_SESSION2_TOKEN); |
| Token token = tokenBundle.getParcelable(KEY_TOKEN); |
| return token == null ? null : new Token(token.mInner, extraSession, session2Token); |
| } |
| |
| public static final Parcelable.Creator<Token> CREATOR |
| = new Parcelable.Creator<Token>() { |
| @SuppressWarnings("deprecation") |
| @Override |
| public Token createFromParcel(Parcel in) { |
| Object inner; |
| if (android.os.Build.VERSION.SDK_INT >= 21) { |
| inner = in.readParcelable(null); |
| } else { |
| inner = in.readStrongBinder(); |
| } |
| return new Token(inner); |
| } |
| |
| @Override |
| public Token[] newArray(int size) { |
| return new Token[size]; |
| } |
| }; |
| } |
| |
| /** |
| * A single item that is part of the play queue. It contains a description |
| * of the item and its id in the queue. |
| */ |
| @SuppressLint("BanParcelableUsage") |
| public static final class QueueItem implements Parcelable { |
| /** |
| * This id is reserved. No items can be explicitly assigned this id. |
| */ |
| public static final int UNKNOWN_ID = -1; |
| |
| private final MediaDescriptionCompat mDescription; |
| private final long mId; |
| |
| private MediaSession.QueueItem mItemFwk; |
| |
| /** |
| * Creates a new {@link MediaSessionCompat.QueueItem}. |
| * |
| * @param description The {@link MediaDescriptionCompat} for this item. |
| * @param id An identifier for this item. It must be unique within the |
| * play queue and cannot be {@link #UNKNOWN_ID}. |
| */ |
| public QueueItem(MediaDescriptionCompat description, long id) { |
| this(null, description, id); |
| } |
| |
| private QueueItem( |
| MediaSession.QueueItem queueItem, |
| MediaDescriptionCompat description, |
| long id) { |
| if (description == null) { |
| throw new IllegalArgumentException("Description cannot be null"); |
| } |
| if (id == UNKNOWN_ID) { |
| throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); |
| } |
| mDescription = description; |
| mId = id; |
| mItemFwk = queueItem; |
| } |
| |
| QueueItem(Parcel in) { |
| mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); |
| mId = in.readLong(); |
| } |
| |
| /** |
| * Gets the description for this item. |
| */ |
| public MediaDescriptionCompat getDescription() { |
| return mDescription; |
| } |
| |
| /** |
| * Gets the queue id for this item. |
| */ |
| public long getQueueId() { |
| return mId; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| mDescription.writeToParcel(dest, flags); |
| dest.writeLong(mId); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| /** |
| * Gets the underlying |
| * {@link android.media.session.MediaSession.QueueItem}. |
| * <p> |
| * On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null |
| * is returned. |
| * |
| * @return The underlying |
| * {@link android.media.session.MediaSession.QueueItem} or null. |
| */ |
| public Object getQueueItem() { |
| if (mItemFwk != null || android.os.Build.VERSION.SDK_INT < 21) { |
| return mItemFwk; |
| } |
| mItemFwk = Api21Impl.createQueueItem( |
| (MediaDescription) mDescription.getMediaDescription(), |
| mId); |
| return mItemFwk; |
| } |
| |
| /** |
| * Creates an instance from a framework {@link android.media.session.MediaSession.QueueItem} |
| * object. |
| * <p> |
| * This method is only supported on API 21+. On API 20 and below, it returns null. |
| * </p> |
| * |
| * @param queueItem A {@link android.media.session.MediaSession.QueueItem} object. |
| * @return An equivalent {@link QueueItem} object, or null if none. |
| */ |
| public static QueueItem fromQueueItem(Object queueItem) { |
| if (queueItem == null || Build.VERSION.SDK_INT < 21) { |
| return null; |
| } |
| MediaSession.QueueItem queueItemObj = (MediaSession.QueueItem) queueItem; |
| Object descriptionObj = Api21Impl.getDescription(queueItemObj); |
| MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription( |
| descriptionObj); |
| long id = Api21Impl.getQueueId(queueItemObj); |
| return new QueueItem(queueItemObj, description, id); |
| } |
| |
| /** |
| * Creates a list of {@link QueueItem} objects from a framework |
| * {@link android.media.session.MediaSession.QueueItem} object list. |
| * <p> |
| * This method is only supported on API 21+. On API 20 and below, it returns null. |
| * </p> |
| * |
| * @param itemList A list of {@link android.media.session.MediaSession.QueueItem} objects. |
| * @return An equivalent list of {@link QueueItem} objects, or null if none. |
| */ |
| public static List<QueueItem> fromQueueItemList(List<?> itemList) { |
| if (itemList == null || Build.VERSION.SDK_INT < 21) { |
| return null; |
| } |
| List<QueueItem> items = new ArrayList<>(itemList.size()); |
| for (Object itemObj : itemList) { |
| items.add(fromQueueItem(itemObj)); |
| } |
| return items; |
| } |
| |
| public static final Creator<MediaSessionCompat.QueueItem> CREATOR |
| = new Creator<MediaSessionCompat.QueueItem>() { |
| |
| @Override |
| public MediaSessionCompat.QueueItem createFromParcel(Parcel p) { |
| return new MediaSessionCompat.QueueItem(p); |
| } |
| |
| @Override |
| public MediaSessionCompat.QueueItem[] newArray(int size) { |
| return new MediaSessionCompat.QueueItem[size]; |
| } |
| }; |
| |
| @Override |
| public String toString() { |
| return "MediaSession.QueueItem {" + |
| "Description=" + mDescription + |
| ", Id=" + mId + " }"; |
| } |
| |
| @RequiresApi(21) |
| private static class Api21Impl { |
| private Api21Impl() {} |
| |
| @DoNotInline |
| static MediaSession.QueueItem createQueueItem(MediaDescription description, long id) { |
| return new MediaSession.QueueItem(description, id); |
| } |
| |
| @DoNotInline |
| static MediaDescription getDescription(MediaSession.QueueItem queueItem) { |
| return queueItem.getDescription(); |
| } |
| |
| @DoNotInline |
| static long getQueueId(MediaSession.QueueItem queueItem) { |
| return queueItem.getQueueId(); |
| } |
| } |
| } |
| |
| /** |
| * This is a wrapper for {@link ResultReceiver} for sending over aidl |
| * interfaces. The framework version was not exposed to aidls until |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. |
| */ |
| @SuppressLint("BanParcelableUsage") |
| /* package */ static final class ResultReceiverWrapper implements Parcelable { |
| ResultReceiver mResultReceiver; |
| |
| public ResultReceiverWrapper(@NonNull ResultReceiver resultReceiver) { |
| mResultReceiver = resultReceiver; |
| } |
| |
| ResultReceiverWrapper(Parcel in) { |
| mResultReceiver = ResultReceiver.CREATOR.createFromParcel(in); |
| } |
| |
| public static final Creator<ResultReceiverWrapper> |
| CREATOR = new Creator<ResultReceiverWrapper>() { |
| @Override |
| public ResultReceiverWrapper createFromParcel(Parcel p) { |
| return new ResultReceiverWrapper(p); |
| } |
| |
| @Override |
| public ResultReceiverWrapper[] newArray(int size) { |
| return new ResultReceiverWrapper[size]; |
| } |
| }; |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| mResultReceiver.writeToParcel(dest, flags); |
| } |
| } |
| |
| public interface OnActiveChangeListener { |
| void onActiveChanged(); |
| } |
| |
| interface MediaSessionImpl { |
| void setCallback(Callback callback, Handler handler); |
| void setRegistrationCallback( |
| @Nullable RegistrationCallback callback, @NonNull Handler handler); |
| void setFlags(@SessionFlags int flags); |
| void setPlaybackToLocal(int stream); |
| void setPlaybackToRemote(VolumeProviderCompat volumeProvider); |
| void setActive(boolean active); |
| boolean isActive(); |
| void sendSessionEvent(String event, Bundle extras); |
| void release(); |
| Token getSessionToken(); |
| void setPlaybackState(PlaybackStateCompat state); |
| PlaybackStateCompat getPlaybackState(); |
| void setMetadata(MediaMetadataCompat metadata); |
| |
| void setSessionActivity(PendingIntent pi); |
| |
| void setMediaButtonReceiver(PendingIntent mbr); |
| void setQueue(List<QueueItem> queue); |
| void setQueueTitle(CharSequence title); |
| |
| void setRatingType(@RatingCompat.Style int type); |
| void setCaptioningEnabled(boolean enabled); |
| void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode); |
| void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode); |
| void setExtras(Bundle extras); |
| |
| Object getMediaSession(); |
| |
| Object getRemoteControlClient(); |
| |
| String getCallingPackage(); |
| RemoteUserInfo getCurrentControllerInfo(); |
| |
| // Internal only method |
| void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo); |
| Callback getCallback(); |
| } |
| |
| static class MediaSessionImplBase implements MediaSessionImpl { |
| /***** RemoteControlClient States, we only need none as the others were public *******/ |
| static final int RCC_PLAYSTATE_NONE = 0; |
| |
| private final Context mContext; |
| private final ComponentName mMediaButtonReceiverComponentName; |
| private final PendingIntent mMediaButtonReceiverIntent; |
| private final MediaSessionStub mStub; |
| private final Token mToken; |
| final Bundle mSessionInfo; |
| final AudioManager mAudioManager; |
| final RemoteControlClient mRcc; |
| |
| final Object mLock = new Object(); |
| final RemoteCallbackList<IMediaControllerCallback> mControllerCallbacks |
| = new RemoteCallbackList<>(); |
| |
| private MessageHandler mHandler; |
| boolean mDestroyed = false; |
| boolean mIsActive = false; |
| volatile Callback mCallback; |
| private RemoteUserInfo mRemoteUserInfo; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| RegistrationCallbackHandler mRegistrationCallbackHandler; |
| |
| // For backward compatibility, these flags are always set. |
| @SessionFlags int mFlags = FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; |
| |
| MediaMetadataCompat mMetadata; |
| PlaybackStateCompat mState; |
| PendingIntent mSessionActivity; |
| List<QueueItem> mQueue; |
| CharSequence mQueueTitle; |
| @RatingCompat.Style int mRatingType; |
| boolean mCaptioningEnabled; |
| @PlaybackStateCompat.RepeatMode int mRepeatMode; |
| @PlaybackStateCompat.ShuffleMode int mShuffleMode; |
| Bundle mExtras; |
| |
| int mVolumeType; |
| int mLocalStream; |
| VolumeProviderCompat mVolumeProvider; |
| |
| private VolumeProviderCompat.Callback mVolumeCallback |
| = new VolumeProviderCompat.Callback() { |
| @Override |
| public void onVolumeChanged(VolumeProviderCompat volumeProvider) { |
| if (mVolumeProvider != volumeProvider) { |
| return; |
| } |
| ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, |
| volumeProvider.getVolumeControl(), volumeProvider.getMaxVolume(), |
| volumeProvider.getCurrentVolume()); |
| sendVolumeInfoChanged(info); |
| } |
| }; |
| |
| public MediaSessionImplBase(Context context, String tag, ComponentName mbrComponent, |
| PendingIntent mbrIntent, VersionedParcelable session2Token, Bundle sessionInfo) { |
| if (mbrComponent == null) { |
| throw new IllegalArgumentException( |
| "MediaButtonReceiver component may not be null"); |
| } |
| mContext = context; |
| mSessionInfo = sessionInfo; |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| mMediaButtonReceiverComponentName = mbrComponent; |
| mMediaButtonReceiverIntent = mbrIntent; |
| mStub = |
| new MediaSessionStub( |
| /* mediaSessionImpl= */ this, context.getPackageName(), tag); |
| mToken = new Token(mStub, /* extraBinder= */ null, session2Token); |
| |
| mRatingType = RatingCompat.RATING_NONE; |
| mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; |
| mLocalStream = AudioManager.STREAM_MUSIC; |
| mRcc = new RemoteControlClient(mbrIntent); |
| } |
| |
| @Override |
| public void setCallback(Callback callback, Handler handler) { |
| synchronized (mLock) { |
| if (mHandler != null) { |
| mHandler.removeCallbacksAndMessages(null); |
| } |
| mHandler = callback == null || handler == null ? null : |
| new MessageHandler(handler.getLooper()); |
| if (mCallback != callback && mCallback != null) { |
| mCallback.setSessionImpl(null, null); |
| } |
| mCallback = callback; |
| if (mCallback != null) { |
| mCallback.setSessionImpl(this, handler); |
| } |
| } |
| } |
| |
| @Override |
| public void setRegistrationCallback( |
| @Nullable RegistrationCallback callback, @NonNull Handler handler) { |
| synchronized (mLock) { |
| if (mRegistrationCallbackHandler != null) { |
| mRegistrationCallbackHandler.removeCallbacksAndMessages(null); |
| } |
| if (callback != null) { |
| mRegistrationCallbackHandler = |
| new RegistrationCallbackHandler(handler.getLooper(), callback); |
| } else { |
| mRegistrationCallbackHandler = null; |
| } |
| } |
| } |
| |
| void postToHandler(int what, int arg1, int arg2, Object obj, Bundle extras) { |
| synchronized (mLock) { |
| if (mHandler != null) { |
| Message msg = mHandler.obtainMessage(what, arg1, arg2, obj); |
| Bundle data = new Bundle(); |
| |
| int uid = Binder.getCallingUid(); |
| data.putInt(DATA_CALLING_UID, uid); |
| // Note: Different apps can have same uid, but only when they are signed with |
| // the same private key. This means those apps are from the same developer. |
| // Session apps can allow/reject controller by reading one of their names. |
| data.putString(DATA_CALLING_PACKAGE, getPackageNameForUid(uid)); |
| int pid = Binder.getCallingPid(); |
| if (pid > 0) { |
| data.putInt(DATA_CALLING_PID, pid); |
| } else { |
| // This cannot be happen for now, but added for future changes. |
| data.putInt(DATA_CALLING_PID, UNKNOWN_PID); |
| } |
| if (extras != null) { |
| data.putBundle(DATA_EXTRAS, extras); |
| } |
| msg.setData(data); |
| msg.sendToTarget(); |
| } |
| } |
| } |
| |
| String getPackageNameForUid(int uid) { |
| String result = mContext.getPackageManager().getNameForUid(uid); |
| if (TextUtils.isEmpty(result)) { |
| result = LEGACY_CONTROLLER; |
| } |
| return result; |
| } |
| |
| @Override |
| public void setFlags(@SessionFlags int flags) { |
| synchronized (mLock) { |
| // For backward compatibility, these flags are always set. |
| mFlags = flags | FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; |
| } |
| } |
| |
| @Override |
| public void setPlaybackToLocal(int stream) { |
| if (mVolumeProvider != null) { |
| mVolumeProvider.setCallback(null); |
| } |
| mLocalStream = stream; |
| mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; |
| ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, |
| VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, |
| mAudioManager.getStreamMaxVolume(mLocalStream), |
| mAudioManager.getStreamVolume(mLocalStream)); |
| sendVolumeInfoChanged(info); |
| } |
| |
| @Override |
| public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { |
| if (volumeProvider == null) { |
| throw new IllegalArgumentException("volumeProvider may not be null"); |
| } |
| if (mVolumeProvider != null) { |
| mVolumeProvider.setCallback(null); |
| } |
| mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; |
| mVolumeProvider = volumeProvider; |
| ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, |
| mVolumeProvider.getVolumeControl(), mVolumeProvider.getMaxVolume(), |
| mVolumeProvider.getCurrentVolume()); |
| sendVolumeInfoChanged(info); |
| |
| volumeProvider.setCallback(mVolumeCallback); |
| } |
| |
| @Override |
| public void setActive(boolean active) { |
| if (active == mIsActive) { |
| return; |
| } |
| mIsActive = active; |
| updateMbrAndRcc(); |
| } |
| |
| @Override |
| public boolean isActive() { |
| return mIsActive; |
| } |
| |
| @Override |
| public void sendSessionEvent(String event, Bundle extras) { |
| sendEvent(event, extras); |
| } |
| |
| @Override |
| public void release() { |
| mIsActive = false; |
| mDestroyed = true; |
| updateMbrAndRcc(); |
| sendSessionDestroyed(); |
| setCallback(null, null); |
| } |
| |
| @Override |
| public Token getSessionToken() { |
| return mToken; |
| } |
| |
| @Override |
| public void setPlaybackState(PlaybackStateCompat state) { |
| synchronized (mLock) { |
| mState = state; |
| } |
| sendState(state); |
| if (!mIsActive) { |
| // Don't set the state until after the RCC is registered |
| return; |
| } |
| if (state == null) { |
| mRcc.setPlaybackState(0); |
| mRcc.setTransportControlFlags(0); |
| } else { |
| // Set state |
| setRccState(state); |
| |
| // Set transport control flags |
| mRcc.setTransportControlFlags( |
| getRccTransportControlFlagsFromActions(state.getActions())); |
| } |
| } |
| |
| @Override |
| public PlaybackStateCompat getPlaybackState() { |
| synchronized (mLock) { |
| return mState; |
| } |
| } |
| |
| void setRccState(PlaybackStateCompat state) { |
| mRcc.setPlaybackState(getRccStateFromState(state.getState())); |
| } |
| |
| int getRccStateFromState(int state) { |
| switch (state) { |
| case PlaybackStateCompat.STATE_CONNECTING: |
| case PlaybackStateCompat.STATE_BUFFERING: |
| return RemoteControlClient.PLAYSTATE_BUFFERING; |
| case PlaybackStateCompat.STATE_ERROR: |
| return RemoteControlClient.PLAYSTATE_ERROR; |
| case PlaybackStateCompat.STATE_FAST_FORWARDING: |
| return RemoteControlClient.PLAYSTATE_FAST_FORWARDING; |
| case PlaybackStateCompat.STATE_NONE: |
| return RCC_PLAYSTATE_NONE; |
| case PlaybackStateCompat.STATE_PAUSED: |
| return RemoteControlClient.PLAYSTATE_PAUSED; |
| case PlaybackStateCompat.STATE_PLAYING: |
| return RemoteControlClient.PLAYSTATE_PLAYING; |
| case PlaybackStateCompat.STATE_REWINDING: |
| return RemoteControlClient.PLAYSTATE_REWINDING; |
| case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS: |
| return RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS; |
| case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT: |
| case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM: |
| return RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS; |
| case PlaybackStateCompat.STATE_STOPPED: |
| return RemoteControlClient.PLAYSTATE_STOPPED; |
| default: |
| return -1; |
| } |
| } |
| |
| int getRccTransportControlFlagsFromActions(long actions) { |
| int transportControlFlags = 0; |
| if ((actions & PlaybackStateCompat.ACTION_STOP) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_STOP; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_PAUSE) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_PLAY) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_REWIND) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_REWIND; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_NEXT; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD; |
| } |
| if ((actions & PlaybackStateCompat.ACTION_PLAY_PAUSE) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; |
| } |
| return transportControlFlags; |
| } |
| |
| @Override |
| public void setMetadata(MediaMetadataCompat metadata) { |
| if (metadata != null) { |
| // Clones {@link MediaMetadataCompat} and scales down bitmaps if they are large. |
| metadata = new MediaMetadataCompat.Builder(metadata, sMaxBitmapSize).build(); |
| } |
| |
| synchronized (mLock) { |
| mMetadata = metadata; |
| } |
| sendMetadata(metadata); |
| if (!mIsActive) { |
| // Don't set metadata until after the rcc has been registered |
| return; |
| } |
| RemoteControlClient.MetadataEditor editor = buildRccMetadata( |
| metadata == null ? null : metadata.getBundle()); |
| editor.apply(); |
| } |
| |
| @SuppressWarnings("deprecation") |
| RemoteControlClient.MetadataEditor buildRccMetadata(Bundle metadata) { |
| RemoteControlClient.MetadataEditor editor = mRcc.editMetadata(true); |
| if (metadata == null) { |
| return editor; |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ART)) { |
| Bitmap art = metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_ART); |
| if (art != null) { |
| // Clone the bitmap to prevent it from being recycled by RCC. |
| art = art.copy(art.getConfig(), false); |
| } |
| editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); |
| } else if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) { |
| // Fall back to album art if the track art wasn't available |
| Bitmap art = metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); |
| if (art != null) { |
| // Clone the bitmap to prevent it from being recycled by RCC. |
| art = art.copy(art.getConfig(), false); |
| } |
| editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ARTIST)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_AUTHOR)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_AUTHOR, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_AUTHOR)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_COMPILATION)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPILATION, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_COMPILATION)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_COMPOSER)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPOSER, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_COMPOSER)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DATE)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_DATE, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_DATE)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)) { |
| editor.putLong(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, |
| metadata.getLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { |
| editor.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, |
| metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_GENRE)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_GENRE, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)) { |
| editor.putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, |
| metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_WRITER)) { |
| editor.putString(MediaMetadataRetriever.METADATA_KEY_WRITER, |
| metadata.getString(MediaMetadataCompat.METADATA_KEY_WRITER)); |
| } |
| return editor; |
| } |
| |
| @Override |
| public void setSessionActivity(PendingIntent pi) { |
| synchronized (mLock) { |
| mSessionActivity = pi; |
| } |
| } |
| |
| @Override |
| public void setMediaButtonReceiver(PendingIntent mbr) { |
| // Do nothing, changing this is not supported before API 21. |
| } |
| |
| @Override |
| public void setQueue(List<QueueItem> queue) { |
| mQueue = queue; |
| sendQueue(queue); |
| } |
| |
| @Override |
| public void setQueueTitle(CharSequence title) { |
| mQueueTitle = title; |
| sendQueueTitle(title); |
| } |
| |
| @Override |
| public Object getMediaSession() { |
| return null; |
| } |
| |
| @Override |
| public Object getRemoteControlClient() { |
| return null; |
| } |
| |
| @Override |
| public String getCallingPackage() { |
| return null; |
| } |
| |
| @Override |
| public void setRatingType(@RatingCompat.Style int type) { |
| mRatingType = type; |
| } |
| |
| @Override |
| public void setCaptioningEnabled(boolean enabled) { |
| if (mCaptioningEnabled != enabled) { |
| mCaptioningEnabled = enabled; |
| sendCaptioningEnabled(enabled); |
| } |
| } |
| |
| @Override |
| public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { |
| if (mRepeatMode != repeatMode) { |
| mRepeatMode = repeatMode; |
| sendRepeatMode(repeatMode); |
| } |
| } |
| |
| @Override |
| public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { |
| if (mShuffleMode != shuffleMode) { |
| mShuffleMode = shuffleMode; |
| sendShuffleMode(shuffleMode); |
| } |
| } |
| |
| @Override |
| public void setExtras(Bundle extras) { |
| mExtras = extras; |
| sendExtras(extras); |
| } |
| |
| @Override |
| public RemoteUserInfo getCurrentControllerInfo() { |
| synchronized (mLock) { |
| return mRemoteUserInfo; |
| } |
| } |
| |
| @Override |
| public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { |
| synchronized (mLock) { |
| mRemoteUserInfo = remoteUserInfo; |
| } |
| } |
| |
| @Override |
| public Callback getCallback() { |
| synchronized (mLock) { |
| return mCallback; |
| } |
| } |
| |
| // Registers/unregisters components as needed. |
| void updateMbrAndRcc() { |
| if (mIsActive) { |
| // When session becomes active, register MBR and RCC. |
| registerMediaButtonEventReceiver(mMediaButtonReceiverIntent, |
| mMediaButtonReceiverComponentName); |
| mAudioManager.registerRemoteControlClient(mRcc); |
| |
| setMetadata(mMetadata); |
| setPlaybackState(mState); |
| } else { |
| // When inactive remove any registered components. |
| unregisterMediaButtonEventReceiver(mMediaButtonReceiverIntent, |
| mMediaButtonReceiverComponentName); |
| // RCC keeps the state while the system resets its state internally when |
| // we register RCC. Reset the state so that the states in RCC and the system |
| // are in sync when we re-register the RCC. |
| mRcc.setPlaybackState(0); |
| mAudioManager.unregisterRemoteControlClient(mRcc); |
| } |
| } |
| |
| void registerMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { |
| mAudioManager.registerMediaButtonEventReceiver(mbrComponent); |
| } |
| |
| void unregisterMediaButtonEventReceiver(PendingIntent mbrIntent, |
| ComponentName mbrComponent) { |
| mAudioManager.unregisterMediaButtonEventReceiver(mbrComponent); |
| } |
| |
| void adjustVolume(int direction, int flags) { |
| if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { |
| if (mVolumeProvider != null) { |
| mVolumeProvider.onAdjustVolume(direction); |
| } |
| } else { |
| mAudioManager.adjustStreamVolume(mLocalStream, direction, flags); |
| } |
| } |
| |
| void setVolumeTo(int value, int flags) { |
| if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { |
| if (mVolumeProvider != null) { |
| mVolumeProvider.onSetVolumeTo(value); |
| } |
| } else { |
| mAudioManager.setStreamVolume(mLocalStream, value, flags); |
| } |
| } |
| |
| void sendVolumeInfoChanged(ParcelableVolumeInfo info) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onVolumeInfoChanged(info); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendSessionDestroyed() { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onSessionDestroyed(); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| mControllerCallbacks.kill(); |
| } |
| } |
| |
| private void sendEvent(String event, Bundle extras) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onEvent(event, extras); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendState(PlaybackStateCompat state) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onPlaybackStateChanged(state); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendMetadata(MediaMetadataCompat metadata) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onMetadataChanged(metadata); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendQueue(List<QueueItem> queue) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onQueueChanged(queue); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendQueueTitle(CharSequence queueTitle) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onQueueTitleChanged(queueTitle); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendCaptioningEnabled(boolean enabled) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onCaptioningEnabledChanged(enabled); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendRepeatMode(int repeatMode) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onRepeatModeChanged(repeatMode); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendShuffleMode(int shuffleMode) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onShuffleModeChanged(shuffleMode); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| private void sendExtras(Bundle extras) { |
| synchronized (mLock) { |
| int size = mControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onExtrasChanged(extras); |
| } catch (RemoteException e) { |
| } |
| } |
| mControllerCallbacks.finishBroadcast(); |
| } |
| } |
| |
| static class MediaSessionStub extends IMediaSession.Stub { |
| |
| private final AtomicReference<MediaSessionImplBase> mMediaSessionImplRef; |
| private final String mPackageName; |
| private final String mTag; |
| |
| MediaSessionStub( |
| MediaSessionImplBase mediaSessionImpl, String packageName, String tag) { |
| mMediaSessionImplRef = new AtomicReference<>(mediaSessionImpl); |
| mPackageName = packageName; |
| mTag = tag; |
| } |
| |
| @Override |
| public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { |
| postToHandler(MessageHandler.MSG_COMMAND, |
| new Command(command, args, cb == null ? null : cb.mResultReceiver)); |
| } |
| |
| @Override |
| public boolean sendMediaButton(KeyEvent mediaButton) { |
| postToHandler(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); |
| return true; |
| } |
| |
| @Override |
| public void registerCallbackListener(IMediaControllerCallback cb) { |
| // If this session is already destroyed tell the caller and |
| // don't add them. |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| try { |
| cb.onSessionDestroyed(); |
| } catch (Exception e) { |
| // ignored |
| } |
| return; |
| } |
| int callingPid = Binder.getCallingPid(); |
| int callingUid = Binder.getCallingUid(); |
| RemoteUserInfo info = |
| new RemoteUserInfo( |
| mediaSessionImpl.getPackageNameForUid(callingUid), |
| callingPid, |
| callingUid); |
| mediaSessionImpl.mControllerCallbacks.register(cb, info); |
| |
| synchronized (mediaSessionImpl.mLock) { |
| if (mediaSessionImpl.mRegistrationCallbackHandler != null) { |
| mediaSessionImpl.mRegistrationCallbackHandler.postCallbackRegistered( |
| callingPid, callingUid); |
| } |
| } |
| } |
| |
| @Override |
| public void unregisterCallbackListener(IMediaControllerCallback cb) { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return; |
| } |
| mediaSessionImpl.mControllerCallbacks.unregister(cb); |
| |
| int callingPid = Binder.getCallingPid(); |
| int callingUid = Binder.getCallingUid(); |
| synchronized (mediaSessionImpl.mLock) { |
| if (mediaSessionImpl.mRegistrationCallbackHandler != null) { |
| mediaSessionImpl.mRegistrationCallbackHandler.postCallbackUnregistered( |
| callingPid, callingUid); |
| } |
| } |
| } |
| |
| @Override |
| public String getPackageName() { |
| return mPackageName; |
| } |
| |
| @Override |
| public Bundle getSessionInfo() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| // mSessionInfo is final so doesn't need synchronize block |
| return mediaSessionImpl != null && mediaSessionImpl.mSessionInfo != null |
| ? new Bundle(mediaSessionImpl.mSessionInfo) |
| : null; |
| } |
| |
| @Override |
| public String getTag() { |
| // mTag is final so doesn't need synchronize block |
| return mTag; |
| } |
| |
| @Override |
| public PendingIntent getLaunchPendingIntent() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return null; |
| } |
| synchronized (mediaSessionImpl.mLock) { |
| return mediaSessionImpl.mSessionActivity; |
| } |
| } |
| |
| @Override |
| @SessionFlags |
| public long getFlags() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return 0; |
| } |
| synchronized (mediaSessionImpl.mLock) { |
| return mediaSessionImpl.mFlags; |
| } |
| } |
| |
| @Override |
| public ParcelableVolumeInfo getVolumeAttributes() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return null; |
| } |
| synchronized (mediaSessionImpl.mLock) { |
| int volumeType = mediaSessionImpl.mVolumeType; |
| int stream = mediaSessionImpl.mLocalStream; |
| VolumeProviderCompat vp = mediaSessionImpl.mVolumeProvider; |
| int controlType; |
| int max; |
| int current; |
| if (volumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { |
| controlType = vp.getVolumeControl(); |
| max = vp.getMaxVolume(); |
| current = vp.getCurrentVolume(); |
| } else { |
| controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; |
| max = mediaSessionImpl.mAudioManager.getStreamMaxVolume(stream); |
| current = mediaSessionImpl.mAudioManager.getStreamVolume(stream); |
| } |
| return new ParcelableVolumeInfo(volumeType, stream, controlType, max, current); |
| } |
| } |
| |
| @Override |
| public void adjustVolume(int direction, int flags, String packageName) { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl != null) { |
| mediaSessionImpl.adjustVolume(direction, flags); |
| } |
| } |
| |
| @Override |
| public void setVolumeTo(int value, int flags, String packageName) { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl != null) { |
| mediaSessionImpl.setVolumeTo(value, flags); |
| } |
| } |
| |
| @Override |
| public void prepare() throws RemoteException { |
| postToHandler(MessageHandler.MSG_PREPARE); |
| } |
| |
| @Override |
| public void prepareFromMediaId(String mediaId, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras); |
| } |
| |
| @Override |
| public void prepareFromSearch(String query, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PREPARE_SEARCH, query, extras); |
| } |
| |
| @Override |
| public void prepareFromUri(Uri uri, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PREPARE_URI, uri, extras); |
| } |
| |
| @Override |
| public void play() throws RemoteException { |
| postToHandler(MessageHandler.MSG_PLAY); |
| } |
| |
| @Override |
| public void playFromMediaId(String mediaId, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); |
| } |
| |
| @Override |
| public void playFromSearch(String query, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PLAY_SEARCH, query, extras); |
| } |
| |
| @Override |
| public void playFromUri(Uri uri, Bundle extras) { |
| postToHandler(MessageHandler.MSG_PLAY_URI, uri, extras); |
| } |
| |
| @Override |
| public void skipToQueueItem(long id) { |
| postToHandler(MessageHandler.MSG_SKIP_TO_ITEM, id); |
| } |
| |
| @Override |
| public void pause() { |
| postToHandler(MessageHandler.MSG_PAUSE); |
| } |
| |
| @Override |
| public void stop() { |
| postToHandler(MessageHandler.MSG_STOP); |
| } |
| |
| @Override |
| public void next() { |
| postToHandler(MessageHandler.MSG_NEXT); |
| } |
| |
| @Override |
| public void previous() { |
| postToHandler(MessageHandler.MSG_PREVIOUS); |
| } |
| |
| @Override |
| public void fastForward() { |
| postToHandler(MessageHandler.MSG_FAST_FORWARD); |
| } |
| |
| @Override |
| public void rewind() { |
| postToHandler(MessageHandler.MSG_REWIND); |
| } |
| |
| @Override |
| public void seekTo(long pos) { |
| postToHandler(MessageHandler.MSG_SEEK_TO, pos); |
| } |
| |
| @Override |
| public void rate(RatingCompat rating) { |
| postToHandler(MessageHandler.MSG_RATE, rating); |
| } |
| |
| @Override |
| public void rateWithExtras(RatingCompat rating, Bundle extras) { |
| postToHandler(MessageHandler.MSG_RATE_EXTRA, rating, extras); |
| } |
| |
| @Override |
| public void setPlaybackSpeed(float speed) { |
| postToHandler(MessageHandler.MSG_SET_PLAYBACK_SPEED, speed); |
| } |
| |
| @Override |
| public void setCaptioningEnabled(boolean enabled) { |
| postToHandler(MessageHandler.MSG_SET_CAPTIONING_ENABLED, enabled); |
| } |
| |
| @Override |
| public void setRepeatMode(int repeatMode) { |
| postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode); |
| } |
| |
| @Override |
| public void setShuffleModeEnabledRemoved(boolean enabled) { |
| // Do nothing. |
| } |
| |
| @Override |
| public void setShuffleMode(int shuffleMode) { |
| postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE, shuffleMode); |
| } |
| |
| @Override |
| public void sendCustomAction(String action, Bundle args) |
| throws RemoteException { |
| postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args); |
| } |
| |
| @Override |
| public MediaMetadataCompat getMetadata() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null ? mediaSessionImpl.mMetadata : null; |
| } |
| |
| @Override |
| public PlaybackStateCompat getPlaybackState() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return null; |
| } |
| PlaybackStateCompat state; |
| MediaMetadataCompat metadata; |
| synchronized (mediaSessionImpl.mLock) { |
| state = mediaSessionImpl.mState; |
| metadata = mediaSessionImpl.mMetadata; |
| } |
| return getStateWithUpdatedPosition(state, metadata); |
| } |
| |
| @Override |
| public List<QueueItem> getQueue() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return null; |
| } |
| synchronized (mediaSessionImpl.mLock) { |
| return mediaSessionImpl.mQueue; |
| } |
| } |
| |
| @Override |
| public void addQueueItem(MediaDescriptionCompat description) { |
| postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM, description); |
| } |
| |
| @Override |
| public void addQueueItemAt(MediaDescriptionCompat description, int index) { |
| postToHandler( |
| MessageHandler.MSG_ADD_QUEUE_ITEM_AT, |
| description, |
| index, |
| /* extras= */ null); |
| } |
| |
| @Override |
| public void removeQueueItem(MediaDescriptionCompat description) { |
| postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM, description); |
| } |
| |
| @Override |
| public void removeQueueItemAt(int index) { |
| postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM_AT, index); |
| } |
| |
| @Override |
| public CharSequence getQueueTitle() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null ? mediaSessionImpl.mQueueTitle : null; |
| } |
| |
| @Override |
| public Bundle getExtras() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl == null) { |
| return null; |
| } |
| synchronized (mediaSessionImpl.mLock) { |
| return mediaSessionImpl.mExtras; |
| } |
| } |
| |
| @Override |
| @RatingCompat.Style |
| public int getRatingType() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null |
| ? mediaSessionImpl.mRatingType |
| : RatingCompat.RATING_NONE; |
| } |
| |
| @Override |
| public boolean isCaptioningEnabled() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null && mediaSessionImpl.mCaptioningEnabled; |
| } |
| |
| @Override |
| @PlaybackStateCompat.RepeatMode |
| public int getRepeatMode() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null |
| ? mediaSessionImpl.mRepeatMode |
| : PlaybackStateCompat.REPEAT_MODE_INVALID; |
| } |
| |
| @Override |
| public boolean isShuffleModeEnabledRemoved() { |
| return false; |
| } |
| |
| @Override |
| @PlaybackStateCompat.ShuffleMode |
| public int getShuffleMode() { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| return mediaSessionImpl != null |
| ? mediaSessionImpl.mShuffleMode |
| : PlaybackStateCompat.SHUFFLE_MODE_INVALID; |
| } |
| |
| @Override |
| public boolean isTransportControlEnabled() { |
| // All sessions should support transport control commands. |
| return true; |
| } |
| |
| void postToHandler(int what) { |
| postToHandler(what, /* obj= */ null, /* arg1= */ 0, /* extras= */ null); |
| } |
| |
| void postToHandler(int what, int arg1) { |
| postToHandler(what, /* obj= */ null, arg1, /* extras= */ null); |
| } |
| |
| void postToHandler(int what, Object obj) { |
| postToHandler(what, obj, /* arg1= */ 0, /* extras= */ null); |
| } |
| |
| void postToHandler(int what, Object obj, Bundle extras) { |
| postToHandler(what, obj, /* arg1= */ 0, extras); |
| } |
| |
| void postToHandler(int what, Object obj, int arg1, Bundle extras) { |
| MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); |
| if (mediaSessionImpl != null) { |
| mediaSessionImpl.postToHandler(what, arg1, /* arg2= */ 0, obj, extras); |
| } |
| } |
| } |
| |
| private static final class Command { |
| public final String command; |
| public final Bundle extras; |
| public final ResultReceiver stub; |
| |
| public Command(String command, Bundle extras, ResultReceiver stub) { |
| this.command = command; |
| this.extras = extras; |
| this.stub = stub; |
| } |
| } |
| |
| class MessageHandler extends Handler { |
| // Next ID: 33 |
| private static final int MSG_COMMAND = 1; |
| private static final int MSG_ADJUST_VOLUME = 2; |
| private static final int MSG_PREPARE = 3; |
| private static final int MSG_PREPARE_MEDIA_ID = 4; |
| private static final int MSG_PREPARE_SEARCH = 5; |
| private static final int MSG_PREPARE_URI = 6; |
| private static final int MSG_PLAY = 7; |
| private static final int MSG_PLAY_MEDIA_ID = 8; |
| private static final int MSG_PLAY_SEARCH = 9; |
| private static final int MSG_PLAY_URI = 10; |
| private static final int MSG_SKIP_TO_ITEM = 11; |
| private static final int MSG_PAUSE = 12; |
| private static final int MSG_STOP = 13; |
| private static final int MSG_NEXT = 14; |
| private static final int MSG_PREVIOUS = 15; |
| private static final int MSG_FAST_FORWARD = 16; |
| private static final int MSG_REWIND = 17; |
| private static final int MSG_SEEK_TO = 18; |
| private static final int MSG_RATE = 19; |
| private static final int MSG_RATE_EXTRA = 31; |
| private static final int MSG_SET_PLAYBACK_SPEED = 32; |
| private static final int MSG_CUSTOM_ACTION = 20; |
| private static final int MSG_MEDIA_BUTTON = 21; |
| private static final int MSG_SET_VOLUME = 22; |
| private static final int MSG_SET_REPEAT_MODE = 23; |
| private static final int MSG_ADD_QUEUE_ITEM = 25; |
| private static final int MSG_ADD_QUEUE_ITEM_AT = 26; |
| private static final int MSG_REMOVE_QUEUE_ITEM = 27; |
| private static final int MSG_REMOVE_QUEUE_ITEM_AT = 28; |
| private static final int MSG_SET_CAPTIONING_ENABLED = 29; |
| private static final int MSG_SET_SHUFFLE_MODE = 30; |
| |
| // KeyEvent constants only available on API 11+ |
| private static final int KEYCODE_MEDIA_PAUSE = 127; |
| private static final int KEYCODE_MEDIA_PLAY = 126; |
| |
| public MessageHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| MediaSessionCompat.Callback cb = mCallback; |
| if (cb == null) { |
| return; |
| } |
| |
| Bundle data = msg.getData(); |
| ensureClassLoader(data); |
| setCurrentControllerInfo(new RemoteUserInfo(data.getString(DATA_CALLING_PACKAGE), |
| data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID))); |
| |
| Bundle extras = data.getBundle(DATA_EXTRAS); |
| ensureClassLoader(extras); |
| |
| try { |
| switch (msg.what) { |
| case MSG_COMMAND: |
| Command cmd = (Command) msg.obj; |
| cb.onCommand(cmd.command, cmd.extras, cmd.stub); |
| break; |
| case MSG_MEDIA_BUTTON: |
| KeyEvent keyEvent = (KeyEvent) msg.obj; |
| Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); |
| intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); |
| // Let the Callback handle events first before using the default |
| // behavior |
| if (!cb.onMediaButtonEvent(intent)) { |
| onMediaButtonEvent(keyEvent, cb); |
| } |
| break; |
| case MSG_PREPARE: |
| cb.onPrepare(); |
| break; |
| case MSG_PREPARE_MEDIA_ID: |
| cb.onPrepareFromMediaId((String) msg.obj, extras); |
| break; |
| case MSG_PREPARE_SEARCH: |
| cb.onPrepareFromSearch((String) msg.obj, extras); |
| break; |
| case MSG_PREPARE_URI: |
| cb.onPrepareFromUri((Uri) msg.obj, extras); |
| break; |
| case MSG_PLAY: |
| cb.onPlay(); |
| break; |
| case MSG_PLAY_MEDIA_ID: |
| cb.onPlayFromMediaId((String) msg.obj, extras); |
| break; |
| case MSG_PLAY_SEARCH: |
| cb.onPlayFromSearch((String) msg.obj, extras); |
| break; |
| case MSG_PLAY_URI: |
| cb.onPlayFromUri((Uri) msg.obj, extras); |
| break; |
| case MSG_SKIP_TO_ITEM: |
| cb.onSkipToQueueItem((Long) msg.obj); |
| break; |
| case MSG_PAUSE: |
| cb.onPause(); |
| break; |
| case MSG_STOP: |
| cb.onStop(); |
| break; |
| case MSG_NEXT: |
| cb.onSkipToNext(); |
| break; |
| case MSG_PREVIOUS: |
| cb.onSkipToPrevious(); |
| break; |
| case MSG_FAST_FORWARD: |
| cb.onFastForward(); |
| break; |
| case MSG_REWIND: |
| cb.onRewind(); |
| break; |
| case MSG_SEEK_TO: |
| cb.onSeekTo((Long) msg.obj); |
| break; |
| case MSG_RATE: |
| cb.onSetRating((RatingCompat) msg.obj); |
| break; |
| case MSG_RATE_EXTRA: |
| cb.onSetRating((RatingCompat) msg.obj, extras); |
| break; |
| case MSG_SET_PLAYBACK_SPEED: |
| cb.onSetPlaybackSpeed((Float) msg.obj); |
| break; |
| case MSG_CUSTOM_ACTION: |
| cb.onCustomAction((String) msg.obj, extras); |
| break; |
| case MSG_ADD_QUEUE_ITEM: |
| cb.onAddQueueItem((MediaDescriptionCompat) msg.obj); |
| break; |
| case MSG_ADD_QUEUE_ITEM_AT: |
| cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1); |
| break; |
| case MSG_REMOVE_QUEUE_ITEM: |
| cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj); |
| break; |
| case MSG_REMOVE_QUEUE_ITEM_AT: |
| if (mQueue != null) { |
| QueueItem item = (msg.arg1 >= 0 && msg.arg1 < mQueue.size()) |
| ? mQueue.get(msg.arg1) : null; |
| if (item != null) { |
| cb.onRemoveQueueItem(item.getDescription()); |
| } |
| } |
| break; |
| case MSG_ADJUST_VOLUME: |
| adjustVolume(msg.arg1, 0); |
| break; |
| case MSG_SET_VOLUME: |
| setVolumeTo(msg.arg1, 0); |
| break; |
| case MSG_SET_CAPTIONING_ENABLED: |
| cb.onSetCaptioningEnabled((boolean) msg.obj); |
| break; |
| case MSG_SET_REPEAT_MODE: |
| cb.onSetRepeatMode(msg.arg1); |
| break; |
| case MSG_SET_SHUFFLE_MODE: |
| cb.onSetShuffleMode(msg.arg1); |
| break; |
| } |
| } finally { |
| setCurrentControllerInfo(null); |
| } |
| } |
| |
| private void onMediaButtonEvent(KeyEvent ke, MediaSessionCompat.Callback cb) { |
| if (ke == null || ke.getAction() != KeyEvent.ACTION_DOWN) { |
| return; |
| } |
| long validActions = mState == null ? 0 : mState.getActions(); |
| switch (ke.getKeyCode()) { |
| // Note KeyEvent.KEYCODE_MEDIA_PLAY is API 11+ |
| case KEYCODE_MEDIA_PLAY: |
| if ((validActions & PlaybackStateCompat.ACTION_PLAY) != 0) { |
| cb.onPlay(); |
| } |
| break; |
| // Note KeyEvent.KEYCODE_MEDIA_PAUSE is API 11+ |
| case KEYCODE_MEDIA_PAUSE: |
| if ((validActions & PlaybackStateCompat.ACTION_PAUSE) != 0) { |
| cb.onPause(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_NEXT: |
| if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { |
| cb.onSkipToNext(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_PREVIOUS: |
| if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { |
| cb.onSkipToPrevious(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_STOP: |
| if ((validActions & PlaybackStateCompat.ACTION_STOP) != 0) { |
| cb.onStop(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: |
| if ((validActions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) { |
| cb.onFastForward(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_REWIND: |
| if ((validActions & PlaybackStateCompat.ACTION_REWIND) != 0) { |
| cb.onRewind(); |
| } |
| break; |
| case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: |
| case KeyEvent.KEYCODE_HEADSETHOOK: |
| Log.w(TAG, "KEYCODE_MEDIA_PLAY_PAUSE and KEYCODE_HEADSETHOOK are handled" |
| + " already"); |
| break; |
| } |
| } |
| } |
| } |
| |
| @RequiresApi(18) |
| static class MediaSessionImplApi18 extends MediaSessionImplBase { |
| private static boolean sIsMbrPendingIntentSupported = true; |
| |
| MediaSessionImplApi18(Context context, String tag, ComponentName mbrComponent, |
| PendingIntent mbrIntent, VersionedParcelable session2Token, Bundle sessionInfo) { |
| super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); |
| } |
| |
| @Override |
| public void setCallback(Callback callback, Handler handler) { |
| super.setCallback(callback, handler); |
| if (callback == null) { |
| mRcc.setPlaybackPositionUpdateListener(null); |
| } else { |
| RemoteControlClient.OnPlaybackPositionUpdateListener listener = |
| new RemoteControlClient.OnPlaybackPositionUpdateListener() { |
| @Override |
| public void onPlaybackPositionUpdate(long newPositionMs) { |
| postToHandler( |
| MessageHandler.MSG_SEEK_TO, -1, -1, newPositionMs, null); |
| } |
| }; |
| mRcc.setPlaybackPositionUpdateListener(listener); |
| } |
| } |
| |
| @Override |
| void setRccState(PlaybackStateCompat state) { |
| long position = state.getPosition(); |
| float speed = state.getPlaybackSpeed(); |
| long updateTime = state.getLastPositionUpdateTime(); |
| long currTime = SystemClock.elapsedRealtime(); |
| if (state.getState() == PlaybackStateCompat.STATE_PLAYING && position > 0) { |
| long diff = 0; |
| if (updateTime > 0) { |
| diff = currTime - updateTime; |
| if (speed > 0 && speed != 1f) { |
| diff = (long) (diff * speed); |
| } |
| } |
| position += diff; |
| } |
| mRcc.setPlaybackState(getRccStateFromState(state.getState()), position, speed); |
| } |
| |
| @Override |
| int getRccTransportControlFlagsFromActions(long actions) { |
| int transportControlFlags = super.getRccTransportControlFlagsFromActions(actions); |
| if ((actions & PlaybackStateCompat.ACTION_SEEK_TO) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; |
| } |
| return transportControlFlags; |
| } |
| |
| @Override |
| void registerMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { |
| // Some Android implementations are not able to register a media button event receiver |
| // using a PendingIntent but need a ComponentName instead. These will raise a |
| // NullPointerException. |
| if (sIsMbrPendingIntentSupported) { |
| try { |
| mAudioManager.registerMediaButtonEventReceiver(mbrIntent); |
| } catch (NullPointerException e) { |
| Log.w(TAG, "Unable to register media button event receiver with " |
| + "PendingIntent, falling back to ComponentName."); |
| sIsMbrPendingIntentSupported = false; |
| } |
| } |
| |
| if (!sIsMbrPendingIntentSupported) { |
| super.registerMediaButtonEventReceiver(mbrIntent, mbrComponent); |
| } |
| } |
| |
| @Override |
| void unregisterMediaButtonEventReceiver(PendingIntent mbrIntent, |
| ComponentName mbrComponent) { |
| if (sIsMbrPendingIntentSupported) { |
| mAudioManager.unregisterMediaButtonEventReceiver(mbrIntent); |
| } else { |
| super.unregisterMediaButtonEventReceiver(mbrIntent, mbrComponent); |
| } |
| } |
| } |
| |
| @RequiresApi(19) |
| static class MediaSessionImplApi19 extends MediaSessionImplApi18 { |
| MediaSessionImplApi19(Context context, String tag, ComponentName mbrComponent, |
| PendingIntent mbrIntent, VersionedParcelable session2Token, Bundle sessionInfo) { |
| super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); |
| } |
| |
| @Override |
| public void setCallback(Callback callback, Handler handler) { |
| super.setCallback(callback, handler); |
| if (callback == null) { |
| mRcc.setMetadataUpdateListener(null); |
| } else { |
| RemoteControlClient.OnMetadataUpdateListener listener = |
| new RemoteControlClient.OnMetadataUpdateListener() { |
| @Override |
| public void onMetadataUpdate(int key, Object newValue) { |
| if (key == MediaMetadataEditor.RATING_KEY_BY_USER |
| && newValue instanceof Rating) { |
| postToHandler(MessageHandler.MSG_RATE, -1, -1, |
| RatingCompat.fromRating(newValue), null); |
| } |
| } |
| }; |
| mRcc.setMetadataUpdateListener(listener); |
| } |
| } |
| |
| @Override |
| int getRccTransportControlFlagsFromActions(long actions) { |
| int transportControlFlags = super.getRccTransportControlFlagsFromActions(actions); |
| if ((actions & PlaybackStateCompat.ACTION_SET_RATING) != 0) { |
| transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_RATING; |
| } |
| return transportControlFlags; |
| } |
| |
| @Override |
| @SuppressWarnings("deprecation") |
| RemoteControlClient.MetadataEditor buildRccMetadata(Bundle metadata) { |
| RemoteControlClient.MetadataEditor editor = super.buildRccMetadata(metadata); |
| long actions = mState == null ? 0 : mState.getActions(); |
| if ((actions & PlaybackStateCompat.ACTION_SET_RATING) != 0) { |
| editor.addEditableKey(RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER); |
| } |
| |
| if (metadata == null) { |
| return editor; |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_YEAR)) { |
| editor.putLong(MediaMetadataRetriever.METADATA_KEY_YEAR, |
| metadata.getLong(MediaMetadataCompat.METADATA_KEY_YEAR)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_RATING)) { |
| // Do not remove casting here. Without this, a crash will happen in API 19. |
| ((MediaMetadataEditor) editor).putObject(MediaMetadataEditor.RATING_KEY_BY_OTHERS, |
| metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_RATING)); |
| } |
| if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_USER_RATING)) { |
| // Do not remove casting here. Without this, a crash will happen in API 19. |
| ((MediaMetadataEditor) editor).putObject(MediaMetadataEditor.RATING_KEY_BY_USER, |
| metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_USER_RATING)); |
| } |
| return editor; |
| } |
| } |
| |
| @RequiresApi(21) |
| static class MediaSessionImplApi21 implements MediaSessionImpl { |
| final MediaSession mSessionFwk; |
| final Token mToken; |
| final Object mLock = new Object(); |
| Bundle mSessionInfo; |
| |
| boolean mDestroyed = false; |
| final RemoteCallbackList<IMediaControllerCallback> mExtraControllerCallbacks = |
| new RemoteCallbackList<>(); |
| |
| PlaybackStateCompat mPlaybackState; |
| List<QueueItem> mQueue; |
| MediaMetadataCompat mMetadata; |
| @RatingCompat.Style int mRatingType; |
| boolean mCaptioningEnabled; |
| @PlaybackStateCompat.RepeatMode int mRepeatMode; |
| @PlaybackStateCompat.ShuffleMode int mShuffleMode; |
| |
| @GuardedBy("mLock") |
| Callback mCallback; |
| @GuardedBy("mLock") |
| RegistrationCallbackHandler mRegistrationCallbackHandler; |
| @GuardedBy("mLock") |
| RemoteUserInfo mRemoteUserInfo; |
| |
| MediaSessionImplApi21(Context context, String tag, VersionedParcelable session2Token, |
| Bundle sessionInfo) { |
| mSessionFwk = createFwkMediaSession(context, tag, sessionInfo); |
| mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession(), session2Token); |
| mSessionInfo = sessionInfo; |
| // For backward compatibility, these flags are always set. |
| setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); |
| } |
| |
| MediaSessionImplApi21(Object mediaSession) { |
| if (!(mediaSession instanceof MediaSession)) { |
| throw new IllegalArgumentException( |
| "mediaSession is not a valid MediaSession object"); |
| } |
| mSessionFwk = (MediaSession) mediaSession; |
| mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession()); |
| mSessionInfo = null; |
| // For backward compatibility, these flags are always set. |
| setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); |
| } |
| |
| public MediaSession createFwkMediaSession(Context context, String tag, Bundle sessionInfo) { |
| return new MediaSession(context, tag); |
| } |
| |
| @Override |
| public void setCallback(Callback callback, Handler handler) { |
| synchronized (mLock) { |
| mCallback = callback; |
| mSessionFwk.setCallback(callback == null ? null : callback.mCallbackFwk, handler); |
| if (callback != null) { |
| callback.setSessionImpl(this, handler); |
| } |
| } |
| } |
| |
| @Override |
| public void setRegistrationCallback( |
| @Nullable RegistrationCallback callback, @NonNull Handler handler) { |
| synchronized (mLock) { |
| if (mRegistrationCallbackHandler != null) { |
| mRegistrationCallbackHandler.removeCallbacksAndMessages(null); |
| } |
| if (callback != null) { |
| mRegistrationCallbackHandler = new RegistrationCallbackHandler( |
| handler.getLooper(), callback); |
| } else { |
| mRegistrationCallbackHandler = null; |
| } |
| } |
| } |
| |
| @SuppressLint("WrongConstant") |
| @Override |
| public void setFlags(@SessionFlags int flags) { |
| // For backward compatibility, always set these deprecated flags. |
| mSessionFwk.setFlags( |
| flags | FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); |
| } |
| |
| @Override |
| public void setPlaybackToLocal(int stream) { |
| // TODO update APIs to use support version of AudioAttributes |
| AudioAttributes.Builder bob = new AudioAttributes.Builder(); |
| bob.setLegacyStreamType(stream); |
| mSessionFwk.setPlaybackToLocal(bob.build()); |
| } |
| |
| @Override |
| public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { |
| mSessionFwk.setPlaybackToRemote((VolumeProvider) volumeProvider.getVolumeProvider()); |
| } |
| |
| @Override |
| public void setActive(boolean active) { |
| mSessionFwk.setActive(active); |
| } |
| |
| @Override |
| public boolean isActive() { |
| return mSessionFwk.isActive(); |
| } |
| |
| @Override |
| public void sendSessionEvent(String event, Bundle extras) { |
| if (android.os.Build.VERSION.SDK_INT < 23) { |
| synchronized (mLock) { |
| int size = mExtraControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onEvent(event, extras); |
| } catch (RemoteException e) { |
| } |
| } |
| mExtraControllerCallbacks.finishBroadcast(); |
| } |
| } |
| mSessionFwk.sendSessionEvent(event, extras); |
| } |
| |
| @Override |
| public void release() { |
| mDestroyed = true; |
| mExtraControllerCallbacks.kill(); |
| if (Build.VERSION.SDK_INT == 27) { |
| // This is a workaround for framework MediaSession's bug in API 27. |
| try { |
| Field callback = mSessionFwk.getClass().getDeclaredField("mCallback"); |
| callback.setAccessible(true); |
| Handler handler = (Handler) callback.get(mSessionFwk); |
| if (handler != null) { |
| handler.removeCallbacksAndMessages(null); |
| } |
| } catch (Exception e) { |
| Log.w(TAG, "Exception happened while accessing MediaSession.mCallback.", |
| e); |
| } |
| } |
| // Prevent from receiving callbacks from released session. |
| mSessionFwk.setCallback(null); |
| mSessionFwk.release(); |
| } |
| |
| @Override |
| public Token getSessionToken() { |
| return mToken; |
| } |
| |
| @Override |
| public void setPlaybackState(PlaybackStateCompat state) { |
| mPlaybackState = state; |
| synchronized (mLock) { |
| int size = mExtraControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onPlaybackStateChanged(state); |
| } catch (RemoteException e) { |
| } |
| } |
| mExtraControllerCallbacks.finishBroadcast(); |
| } |
| mSessionFwk.setPlaybackState( |
| state == null ? null : (PlaybackState) state.getPlaybackState()); |
| } |
| |
| @Override |
| public PlaybackStateCompat getPlaybackState() { |
| return mPlaybackState; |
| } |
| |
| @Override |
| public void setMetadata(MediaMetadataCompat metadata) { |
| mMetadata = metadata; |
| mSessionFwk.setMetadata( |
| metadata == null ? null : (MediaMetadata) metadata.getMediaMetadata()); |
| } |
| |
| @Override |
| public void setSessionActivity(PendingIntent pi) { |
| mSessionFwk.setSessionActivity(pi); |
| } |
| |
| @Override |
| public void setMediaButtonReceiver(PendingIntent mbr) { |
| mSessionFwk.setMediaButtonReceiver(mbr); |
| } |
| |
| @Override |
| public void setQueue(List<QueueItem> queue) { |
| mQueue = queue; |
| if (queue == null) { |
| mSessionFwk.setQueue(null); |
| return; |
| } |
| ArrayList<MediaSession.QueueItem> queueItemFwks = new ArrayList<>(queue.size()); |
| for (QueueItem item : queue) { |
| queueItemFwks.add((MediaSession.QueueItem) item.getQueueItem()); |
| } |
| mSessionFwk.setQueue(queueItemFwks); |
| } |
| |
| @Override |
| public void setQueueTitle(CharSequence title) { |
| mSessionFwk.setQueueTitle(title); |
| } |
| |
| @Override |
| public void setRatingType(@RatingCompat.Style int type) { |
| mRatingType = type; |
| } |
| |
| @Override |
| public void setCaptioningEnabled(boolean enabled) { |
| if (mCaptioningEnabled != enabled) { |
| mCaptioningEnabled = enabled; |
| synchronized (mLock) { |
| int size = mExtraControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onCaptioningEnabledChanged(enabled); |
| } catch (RemoteException e) { |
| } |
| } |
| mExtraControllerCallbacks.finishBroadcast(); |
| } |
| } |
| } |
| |
| @Override |
| public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { |
| if (mRepeatMode != repeatMode) { |
| mRepeatMode = repeatMode; |
| synchronized (mLock) { |
| int size = mExtraControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onRepeatModeChanged(repeatMode); |
| } catch (RemoteException e) { |
| } |
| } |
| mExtraControllerCallbacks.finishBroadcast(); |
| } |
| } |
| } |
| |
| @Override |
| public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { |
| if (mShuffleMode != shuffleMode) { |
| mShuffleMode = shuffleMode; |
| synchronized (mLock) { |
| int size = mExtraControllerCallbacks.beginBroadcast(); |
| for (int i = size - 1; i >= 0; i--) { |
| IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); |
| try { |
| cb.onShuffleModeChanged(shuffleMode); |
| } catch (RemoteException e) { |
| } |
| } |
| mExtraControllerCallbacks.finishBroadcast(); |
| } |
| } |
| } |
| |
| @Override |
| public void setExtras(Bundle extras) { |
| mSessionFwk.setExtras(extras); |
| } |
| |
| @Override |
| public Object getMediaSession() { |
| return mSessionFwk; |
| } |
| |
| @Override |
| public Object getRemoteControlClient() { |
| // Note: When this returns somthing, {@link MediaSessionCompatCallbackTest} and |
| // {@link #setCurrentUserInfoOverride} should be also updated. |
| return null; |
| } |
| |
| @Override |
| public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { |
| synchronized (mLock) { |
| mRemoteUserInfo = remoteUserInfo; |
| } |
| } |
| |
| @Override |
| public String getCallingPackage() { |
| if (android.os.Build.VERSION.SDK_INT < 24) { |
| return null; |
| } else { |
| try { |
| Method getCallingPackageMethod = mSessionFwk.getClass().getMethod( |
| "getCallingPackage"); |
| return (String) getCallingPackageMethod.invoke(mSessionFwk); |
| } catch (Exception e) { |
| Log.e(TAG, "Cannot execute MediaSession.getCallingPackage()", e); |
| } |
| return null; |
| } |
| } |
| |
| @Override |
| public RemoteUserInfo getCurrentControllerInfo() { |
| synchronized (mLock) { |
| return mRemoteUserInfo; |
| } |
| } |
| |
| @Override |
| public Callback getCallback() { |
| synchronized (mLock) { |
| return mCallback; |
| } |
| } |
| |
| class ExtraSession extends IMediaSession.Stub { |
| @Override |
| public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public boolean sendMediaButton(KeyEvent mediaButton) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void registerCallbackListener(IMediaControllerCallback cb) { |
| if (mDestroyed) { |
| return; |
| } |
| int callingPid = Binder.getCallingPid(); |
| int callingUid = Binder.getCallingUid(); |
| RemoteUserInfo info = new RemoteUserInfo( |
| RemoteUserInfo.LEGACY_CONTROLLER, callingPid, callingUid); |
| mExtraControllerCallbacks.register(cb, info); |
| synchronized (mLock) { |
| if (mRegistrationCallbackHandler != null) { |
| mRegistrationCallbackHandler.postCallbackRegistered(callingPid, callingUid); |
| } |
| } |
| } |
| |
| @Override |
| public void unregisterCallbackListener(IMediaControllerCallback cb) { |
| mExtraControllerCallbacks.unregister(cb); |
| |
| int callingPid = Binder.getCallingPid(); |
| int callingUid = Binder.getCallingUid(); |
| synchronized (mLock) { |
| if (mRegistrationCallbackHandler != null) { |
| mRegistrationCallbackHandler.postCallbackUnregistered( |
| callingPid, callingUid); |
| } |
| } |
| } |
| |
| @Override |
| public String getPackageName() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public Bundle getSessionInfo() { |
| return mSessionInfo == null ? null : new Bundle(mSessionInfo); |
| } |
| |
| @Override |
| public String getTag() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public PendingIntent getLaunchPendingIntent() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| @SessionFlags |
| public long getFlags() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public ParcelableVolumeInfo getVolumeAttributes() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void adjustVolume(int direction, int flags, String packageName) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void setVolumeTo(int value, int flags, String packageName) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void prepare() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void prepareFromMediaId(String mediaId, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void prepareFromSearch(String query, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void prepareFromUri(Uri uri, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void play() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void playFromSearch(String query, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void playFromUri(Uri uri, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void skipToQueueItem(long id) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void pause() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void stop() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void next() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void previous() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void fastForward() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void rewind() throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void seekTo(long pos) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void rate(RatingCompat rating) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void rateWithExtras(RatingCompat rating, Bundle extras) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void setPlaybackSpeed(float speed) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void setCaptioningEnabled(boolean enabled) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void setRepeatMode(int repeatMode) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void setShuffleModeEnabledRemoved(boolean enabled) throws RemoteException { |
| // Do nothing. |
| } |
| |
| @Override |
| public void setShuffleMode(int shuffleMode) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void sendCustomAction(String action, Bundle args) throws RemoteException { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public MediaMetadataCompat getMetadata() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public PlaybackStateCompat getPlaybackState() { |
| return getStateWithUpdatedPosition(mPlaybackState, mMetadata); |
| } |
| |
| @Override |
| public List<QueueItem> getQueue() { |
| // Will not be called. |
| return null; |
| } |
| |
| @Override |
| public void addQueueItem(MediaDescriptionCompat descriptionCompat) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void addQueueItemAt(MediaDescriptionCompat descriptionCompat, int index) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void removeQueueItem(MediaDescriptionCompat description) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public void removeQueueItemAt(int index) { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public CharSequence getQueueTitle() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| public Bundle getExtras() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| |
| @Override |
| @RatingCompat.Style |
| public int getRatingType() { |
| return mRatingType; |
| } |
| |
| @Override |
| public boolean isCaptioningEnabled() { |
| return mCaptioningEnabled; |
| } |
| |
| @Override |
| @PlaybackStateCompat.RepeatMode |
| public int getRepeatMode() { |
| return mRepeatMode; |
| } |
| |
| @Override |
| public boolean isShuffleModeEnabledRemoved() { |
| return false; |
| } |
| |
| @Override |
| @PlaybackStateCompat.ShuffleMode |
| public int getShuffleMode() { |
| return mShuffleMode; |
| } |
| |
| @Override |
| public boolean isTransportControlEnabled() { |
| // Will not be called. |
| throw new AssertionError(); |
| } |
| } |
| } |
| |
| @RequiresApi(22) |
| static class MediaSessionImplApi22 extends MediaSessionImplApi21 { |
| MediaSessionImplApi22(Context context, String tag, VersionedParcelable session2Token, |
| Bundle sessionInfo) { |
| super(context, tag, session2Token, sessionInfo); |
| } |
| |
| MediaSessionImplApi22(Object mediaSession) { |
| super(mediaSession); |
| } |
| |
| @Override |
| public void setRatingType(@RatingCompat.Style int type) { |
| mSessionFwk.setRatingType(type); |
| } |
| } |
| |
| @RequiresApi(28) |
| static class MediaSessionImplApi28 extends MediaSessionImplApi22 { |
| MediaSessionImplApi28(Context context, String tag, VersionedParcelable session2Token, |
| Bundle sessionInfo) { |
| super(context, tag, session2Token, sessionInfo); |
| } |
| |
| MediaSessionImplApi28(Object mediaSession) { |
| super(mediaSession); |
| } |
| |
| @Override |
| public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { |
| // No-op. {@link MediaSession#getCurrentControllerInfo} would work. |
| } |
| |
| @Override |
| @NonNull |
| public final RemoteUserInfo getCurrentControllerInfo() { |
| android.media.session.MediaSessionManager.RemoteUserInfo info = |
| ((MediaSession) mSessionFwk).getCurrentControllerInfo(); |
| return new RemoteUserInfo(info); |
| } |
| } |
| |
| @RequiresApi(29) |
| static class MediaSessionImplApi29 extends MediaSessionImplApi28 { |
| MediaSessionImplApi29(Context context, String tag, VersionedParcelable session2Token, |
| Bundle sessionInfo) { |
| super(context, tag, session2Token, sessionInfo); |
| } |
| |
| MediaSessionImplApi29(Object mediaSession) { |
| super(mediaSession); |
| mSessionInfo = ((MediaSession) mediaSession).getController().getSessionInfo(); |
| } |
| |
| @Override |
| public MediaSession createFwkMediaSession(Context context, String tag, Bundle sessionInfo) { |
| return new MediaSession(context, tag, sessionInfo); |
| } |
| } |
| |
| static final class RegistrationCallbackHandler extends Handler { |
| private static final int MSG_CALLBACK_REGISTERED = 1001; |
| private static final int MSG_CALLBACK_UNREGISTERED = 1002; |
| |
| private final RegistrationCallback mCallback; |
| |
| RegistrationCallbackHandler( |
| @NonNull Looper looper, |
| @NonNull RegistrationCallback callback) { |
| super(looper); |
| mCallback = callback; |
| } |
| |
| @Override |
| public void handleMessage(@NonNull Message msg) { |
| super.handleMessage(msg); |
| switch (msg.what) { |
| case MSG_CALLBACK_REGISTERED: |
| mCallback.onCallbackRegistered(msg.arg1, msg.arg2); |
| break; |
| case MSG_CALLBACK_UNREGISTERED: |
| mCallback.onCallbackUnregistered(msg.arg1, msg.arg2); |
| break; |
| } |
| } |
| |
| public void postCallbackRegistered(int callingPid, int callingUid) { |
| obtainMessage(MSG_CALLBACK_REGISTERED, callingPid, callingUid).sendToTarget(); |
| } |
| |
| public void postCallbackUnregistered(int callingPid, int callingUid) { |
| obtainMessage(MSG_CALLBACK_UNREGISTERED, callingPid, callingUid).sendToTarget(); |
| } |
| } |
| } |