From ed15ab012ff8d06c0b426761257a4ec3660ce31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 17 Jul 2024 16:18:07 +0200 Subject: [PATCH] Revert changes to `androidx.media3.session.legacy` --- .../session/legacy/AudioAttributesCompat.java | 13 +- .../session/legacy/LegacyParcelableUtil.java | 2 +- .../session/legacy/MediaBrowserCompat.java | 57 +- .../legacy/MediaBrowserServiceCompat.java | 7 +- .../session/legacy/MediaControllerCompat.java | 64 +- .../legacy/MediaDescriptionCompat.java | 155 +- .../session/legacy/MediaMetadataCompat.java | 9 +- .../session/legacy/MediaSessionCompat.java | 1764 ++++++++++++++++- .../session/legacy/MediaSessionManager.java | 5 +- .../session/legacy/PlaybackStateCompat.java | 200 +- .../media3/session/legacy/RatingCompat.java | 4 + .../session/legacy/VolumeProviderCompat.java | 40 +- .../legacy/AudioAttributesCompatTest.java | 9 + .../legacy/MediaDescriptionCompatTest.java | 3 + 14 files changed, 2206 insertions(+), 126 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java index 071dbe699a..14750fb5d0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java @@ -308,9 +308,10 @@ public class AudioAttributesCompat { } if (Build.VERSION.SDK_INT >= 26) { return new AudioAttributesCompat(new AudioAttributesImplApi26((AudioAttributes) aa)); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { return new AudioAttributesCompat(new AudioAttributesImplApi21((AudioAttributes) aa)); } + return null; } // The rest of this file implements an approximation to AudioAttributes using old stream types @@ -376,8 +377,10 @@ public class AudioAttributesCompat { mBuilderImpl = new AudioAttributesImplBase.Builder(); } else if (Build.VERSION.SDK_INT >= 26) { mBuilderImpl = new AudioAttributesImplApi26.Builder(); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mBuilderImpl = new AudioAttributesImplApi21.Builder(); + } else { + mBuilderImpl = new AudioAttributesImplBase.Builder(); } } @@ -391,8 +394,10 @@ public class AudioAttributesCompat { mBuilderImpl = new AudioAttributesImplBase.Builder(aa); } else if (Build.VERSION.SDK_INT >= 26) { mBuilderImpl = new AudioAttributesImplApi26.Builder(checkNotNull(aa.unwrap())); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mBuilderImpl = new AudioAttributesImplApi21.Builder(checkNotNull(aa.unwrap())); + } else { + mBuilderImpl = new AudioAttributesImplBase.Builder(aa); } } @@ -938,6 +943,7 @@ public class AudioAttributesCompat { } } + @RequiresApi(21) public static class AudioAttributesImplApi21 implements AudioAttributesImpl { @Nullable public AudioAttributes mAudioAttributes; @@ -1014,6 +1020,7 @@ public class AudioAttributesCompat { return "AudioAttributesCompat: audioattributes=" + mAudioAttributes; } + @RequiresApi(21) static class Builder implements AudioAttributesImpl.Builder { final AudioAttributes.Builder mFwkBuilder; diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java b/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java index 87b6b64ec3..ebe6eb6daa 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java @@ -88,7 +88,7 @@ public final class LegacyParcelableUtil { // TODO: b/335804969 - Remove this workaround once the bug fix is in the androidx.media dependency @SuppressWarnings("unchecked") private static T maybeApplyMediaDescriptionParcelableBugWorkaround(T value) { - if (Util.SDK_INT >= 23) { + if (Util.SDK_INT < 21 || Util.SDK_INT >= 23) { return value; } if (value instanceof android.support.v4.media.MediaBrowserCompat.MediaItem) { diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java index 7fe7714339..d46d437001 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java @@ -56,6 +56,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.media.MediaDescription; import android.media.browse.MediaBrowser; import android.os.BadParcelableException; import android.os.Binder; @@ -72,8 +73,8 @@ import android.os.RemoteException; import android.support.v4.os.ResultReceiver; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.DoNotInline; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; @@ -204,8 +205,10 @@ public final class MediaBrowserCompat { mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints); } else if (Build.VERSION.SDK_INT >= 23) { mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints); + } else { + mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints); } } @@ -455,18 +458,20 @@ public final class MediaBrowserCompat { * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem} * object. * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object. * @return An equivalent {@link MediaItem} object, or null if none. */ @Nullable public static MediaItem fromMediaItem(@Nullable Object itemObj) { - if (itemObj == null) { + if (itemObj == null || Build.VERSION.SDK_INT < 21) { return null; } MediaBrowser.MediaItem itemFwk = (MediaBrowser.MediaItem) itemObj; - int flags = itemFwk.getFlags(); + int flags = Api21Impl.getFlags(itemFwk); MediaDescriptionCompat descriptionCompat = - MediaDescriptionCompat.fromMediaDescription(itemFwk.getDescription()); + MediaDescriptionCompat.fromMediaDescription(Api21Impl.getDescription(itemFwk)); return new MediaItem(descriptionCompat, flags); } @@ -474,12 +479,14 @@ public final class MediaBrowserCompat { * Creates a list of {@link MediaItem} objects from a framework {@link * android.media.browse.MediaBrowser.MediaItem} object list. * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects. * @return An equivalent list of {@link MediaItem} objects, or null if none. */ @Nullable public static List fromMediaItemList(@Nullable List itemList) { - if (itemList == null) { + if (itemList == null || Build.VERSION.SDK_INT < 21) { return null; } List items = new ArrayList<>(itemList.size()); @@ -594,7 +601,11 @@ public final class MediaBrowserCompat { @Nullable ConnectionCallbackInternal mConnectionCallbackInternal; public ConnectionCallback() { - mConnectionCallbackFwk = new ConnectionCallbackApi21(); + if (Build.VERSION.SDK_INT >= 21) { + mConnectionCallbackFwk = new ConnectionCallbackApi21(); + } else { + mConnectionCallbackFwk = null; + } } /** @@ -636,6 +647,7 @@ public final class MediaBrowserCompat { void onConnectionFailed(); } + @RequiresApi(21) private class ConnectionCallbackApi21 extends MediaBrowser.ConnectionCallback { ConnectionCallbackApi21() {} @@ -667,7 +679,7 @@ public final class MediaBrowserCompat { /** Callbacks for subscription related events. */ public abstract static class SubscriptionCallback { - @NonNull final MediaBrowser.SubscriptionCallback mSubscriptionCallbackFwk; + @Nullable final MediaBrowser.SubscriptionCallback mSubscriptionCallbackFwk; final IBinder mToken; @Nullable WeakReference mSubscriptionRef; @@ -675,8 +687,10 @@ public final class MediaBrowserCompat { mToken = new Binder(); if (Build.VERSION.SDK_INT >= 26) { mSubscriptionCallbackFwk = new SubscriptionCallbackApi26(); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mSubscriptionCallbackFwk = new SubscriptionCallbackApi21(); + } else { + mSubscriptionCallbackFwk = null; } } @@ -724,6 +738,7 @@ public final class MediaBrowserCompat { mSubscriptionRef = new WeakReference<>(subscription); } + @RequiresApi(21) private class SubscriptionCallbackApi21 extends MediaBrowser.SubscriptionCallback { SubscriptionCallbackApi21() {} @@ -1628,6 +1643,7 @@ public final class MediaBrowserCompat { } } + @RequiresApi(21) static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl, @@ -1728,7 +1744,7 @@ public final class MediaBrowserCompat { if (mServiceBinderWrapper == null) { // TODO: When MediaBrowser is connected to framework's MediaBrowserService, // subscribe with options won't work properly. - mBrowserFwk.subscribe(parentId, callback.mSubscriptionCallbackFwk); + mBrowserFwk.subscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); } else { try { mServiceBinderWrapper.addSubscription( @@ -2076,9 +2092,9 @@ public final class MediaBrowserCompat { // This is to prevent ClassNotFoundException when options has Parcelable in it. if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) { if (options == null) { - mBrowserFwk.subscribe(parentId, callback.mSubscriptionCallbackFwk); + mBrowserFwk.subscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); } else { - mBrowserFwk.subscribe(parentId, options, callback.mSubscriptionCallbackFwk); + mBrowserFwk.subscribe(parentId, options, checkNotNull(callback.mSubscriptionCallbackFwk)); } } else { super.subscribe(parentId, options, callback); @@ -2093,7 +2109,7 @@ public final class MediaBrowserCompat { if (callback == null) { mBrowserFwk.unsubscribe(parentId); } else { - mBrowserFwk.unsubscribe(parentId, callback.mSubscriptionCallbackFwk); + mBrowserFwk.unsubscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); } } else { super.unsubscribe(parentId, callback); @@ -2450,4 +2466,19 @@ public final class MediaBrowserCompat { } } } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static MediaDescription getDescription(MediaBrowser.MediaItem item) { + return item.getDescription(); + } + + @DoNotInline + static int getFlags(MediaBrowser.MediaItem item) { + return item.getFlags(); + } + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java index f52431c06f..7f670a52d7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java @@ -307,6 +307,7 @@ public abstract class MediaBrowserServiceCompat extends Service { } } + @RequiresApi(21) class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl { final List mRootExtrasList = new ArrayList<>(); @MonotonicNonNull MediaBrowserService mServiceFwk; @@ -519,6 +520,7 @@ public abstract class MediaBrowserServiceCompat extends Service { return mCurConnection.browserInfo; } + @RequiresApi(21) class MediaBrowserServiceApi21 extends MediaBrowserService { @SuppressWarnings("method.invocation.invalid") // Calling base method from constructor MediaBrowserServiceApi21(Context context) { @@ -1266,6 +1268,7 @@ public abstract class MediaBrowserServiceCompat extends Service { } } + @RequiresApi(21) @SuppressWarnings({"rawtypes", "unchecked"}) static class ResultWrapper { MediaBrowserService.Result mResultFwk; @@ -1328,8 +1331,10 @@ public abstract class MediaBrowserServiceCompat extends Service { mImpl = new MediaBrowserServiceImplApi26(); } else if (Build.VERSION.SDK_INT >= 23) { mImpl = new MediaBrowserServiceImplApi23(); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mImpl = new MediaBrowserServiceImplApi21(); + } else { + mImpl = new MediaBrowserServiceImplBase(); } mImpl.onCreate(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java index efc76e7e7f..790d58eb69 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java @@ -40,7 +40,6 @@ import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; @@ -143,12 +142,9 @@ public final class MediaControllerCompat { .getWindow() .getDecorView() .setTag(R.id.media_controller_compat_view_tag, mediaController); - MediaController controllerFwk = null; - if (mediaController != null) { - Object sessionTokenObj = mediaController.getSessionToken().getToken(); - controllerFwk = new MediaController(activity, (MediaSession.Token) sessionTokenObj); + if (android.os.Build.VERSION.SDK_INT >= 21) { + MediaControllerImplApi21.setMediaController(activity, mediaController); } - activity.setMediaController(controllerFwk); } /** @@ -166,15 +162,10 @@ public final class MediaControllerCompat { Object tag = activity.getWindow().getDecorView().getTag(R.id.media_controller_compat_view_tag); if (tag instanceof MediaControllerCompat) { return (MediaControllerCompat) tag; - } else { - MediaController controllerFwk = activity.getMediaController(); - if (controllerFwk == null) { - return null; - } - MediaSession.Token sessionTokenFwk = controllerFwk.getSessionToken(); - return new MediaControllerCompat( - activity, MediaSessionCompat.Token.fromToken(sessionTokenFwk)); + } else if (android.os.Build.VERSION.SDK_INT >= 21) { + return MediaControllerImplApi21.getMediaController(activity); } + return null; } @SuppressWarnings("WeakerAccess") /* synthetic access */ @@ -230,8 +221,10 @@ public final class MediaControllerCompat { if (Build.VERSION.SDK_INT >= 29) { mImpl = new MediaControllerImplApi29(context, sessionToken); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mImpl = new MediaControllerImplApi21(context, sessionToken); + } else { + mImpl = new MediaControllerImplBase(sessionToken); } } @@ -642,6 +635,8 @@ public final class MediaControllerCompat { /** * Gets the underlying framework {@link android.media.session.MediaController} object. * + *

This method is only supported on API 21+. + * * @return The underlying {@link android.media.session.MediaController} object, or null if none. */ @Nullable @@ -654,14 +649,19 @@ public final class MediaControllerCompat { * #registerCallback} */ public abstract static class Callback implements IBinder.DeathRecipient { - @NonNull final MediaController.Callback mCallbackFwk; + @Nullable final MediaController.Callback mCallbackFwk; @Nullable MessageHandler mHandler; @Nullable IMediaControllerCallback mIControllerCallback; // Sharing this in constructor @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible"}) public Callback() { - mCallbackFwk = new MediaControllerCallbackApi21(this); + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackFwk = new MediaControllerCallbackApi21(this); + } else { + mCallbackFwk = null; + mIControllerCallback = new StubCompat(this); + } } /** @@ -789,6 +789,7 @@ public final class MediaControllerCompat { } // Callback methods in this class are run on handler which was given to registerCallback(). + @RequiresApi(21) private static class MediaControllerCallbackApi21 extends MediaController.Callback { private final WeakReference mCallback; @@ -1973,6 +1974,7 @@ public final class MediaControllerCompat { } } + @RequiresApi(21) static class MediaControllerImplApi21 implements MediaControllerImpl { protected final MediaController mControllerFwk; @@ -2000,7 +2002,7 @@ public final class MediaControllerCompat { @Override public final void registerCallback(Callback callback, Handler handler) { - mControllerFwk.registerCallback(callback.mCallbackFwk, handler); + mControllerFwk.registerCallback(checkNotNull(callback.mCallbackFwk), handler); synchronized (mLock) { IMediaSession extraBinder = mSessionToken.getExtraBinder(); if (extraBinder != null) { @@ -2022,7 +2024,7 @@ public final class MediaControllerCompat { @Override public final void unregisterCallback(Callback callback) { - mControllerFwk.unregisterCallback(callback.mCallbackFwk); + mControllerFwk.unregisterCallback(checkNotNull(callback.mCallbackFwk)); synchronized (mLock) { IMediaSession extraBinder = mSessionToken.getExtraBinder(); if (extraBinder != null) { @@ -2149,7 +2151,7 @@ public final class MediaControllerCompat { @Override public int getRatingType() { - if (android.os.Build.VERSION.SDK_INT == 21) { + if (android.os.Build.VERSION.SDK_INT < 22) { try { IMediaSession extraBinder = mSessionToken.getExtraBinder(); if (extraBinder != null) { @@ -2302,6 +2304,27 @@ public final class MediaControllerCompat { mPendingCallbacks.clear(); } + @SuppressWarnings("argument.type.incompatible") // Activity.setMediaController is not annotated + static void setMediaController(Activity activity, MediaControllerCompat mediaControllerCompat) { + MediaController controllerFwk = null; + if (mediaControllerCompat != null) { + Object sessionTokenObj = mediaControllerCompat.getSessionToken().getToken(); + controllerFwk = new MediaController(activity, (MediaSession.Token) sessionTokenObj); + } + activity.setMediaController(controllerFwk); + } + + @Nullable + static MediaControllerCompat getMediaController(Activity activity) { + MediaController controllerFwk = activity.getMediaController(); + if (controllerFwk == null) { + return null; + } + MediaSession.Token sessionTokenFwk = controllerFwk.getSessionToken(); + return new MediaControllerCompat( + activity, MediaSessionCompat.Token.fromToken(sessionTokenFwk)); + } + private static class ExtraBinderRequestResultReceiver extends ResultReceiver { private WeakReference mMediaControllerImpl; @@ -2388,6 +2411,7 @@ public final class MediaControllerCompat { } } + @RequiresApi(21) static class TransportControlsApi21 extends TransportControls { protected final MediaController.TransportControls mControlsFwk; diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java index 7f7e4e7eba..7019bd0ca2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java @@ -294,7 +294,18 @@ public final class MediaDescriptionCompat implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - ((MediaDescription) getMediaDescription()).writeToParcel(dest, flags); + if (Build.VERSION.SDK_INT < 21) { + dest.writeString(mMediaId); + TextUtils.writeToParcel(mTitle, dest, flags); + TextUtils.writeToParcel(mSubtitle, dest, flags); + TextUtils.writeToParcel(mDescription, dest, flags); + dest.writeParcelable(mIcon, flags); + dest.writeParcelable(mIconUri, flags); + dest.writeBundle(mExtras); + dest.writeParcelable(mMediaUri, flags); + } else { + ((MediaDescription) getMediaDescription()).writeToParcel(dest, flags); + } } @Override @@ -305,19 +316,22 @@ public final class MediaDescriptionCompat implements Parcelable { /** * Gets the underlying framework {@link android.media.MediaDescription} object. * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * * @return An equivalent {@link android.media.MediaDescription} object, or null if none. */ + @RequiresApi(21) public Object getMediaDescription() { if (mDescriptionFwk != null) { return mDescriptionFwk; } - MediaDescription.Builder bob = new MediaDescription.Builder() - .setMediaId(mMediaId) - .setTitle(mTitle) - .setSubtitle(mSubtitle) - .setDescription(mDescription) - .setIconBitmap(mIcon) - .setIconUri(mIconUri); + MediaDescription.Builder bob = Api21Impl.createBuilder(); + Api21Impl.setMediaId(bob, mMediaId); + Api21Impl.setTitle(bob, mTitle); + Api21Impl.setSubtitle(bob, mSubtitle); + Api21Impl.setDescription(bob, mDescription); + Api21Impl.setIconBitmap(bob, mIcon); + Api21Impl.setIconUri(bob, mIconUri); // Media URI was not added until API 23, so add it to the Bundle of extras to // ensure the data is not lost - this ensures that // fromMediaDescription(getMediaDescription(mediaDescriptionCompat)) returns @@ -331,14 +345,14 @@ public final class MediaDescriptionCompat implements Parcelable { extras = new Bundle(mExtras); } extras.putParcelable(DESCRIPTION_KEY_MEDIA_URI, mMediaUri); - bob.setExtras(extras); + Api21Impl.setExtras(bob, extras); } else { - bob.setExtras(mExtras); + Api21Impl.setExtras(bob, mExtras); } if (Build.VERSION.SDK_INT >= 23) { Api23Impl.setMediaUri(bob, mMediaUri); } - mDescriptionFwk = bob.build(); + mDescriptionFwk = Api21Impl.build(bob); return mDescriptionFwk; } @@ -346,22 +360,24 @@ public final class MediaDescriptionCompat implements Parcelable { /** * Creates an instance from a framework {@link android.media.MediaDescription} object. * + *

This method is only supported on API 21+. + * * @param descriptionObj A {@link android.media.MediaDescription} object, or null if none. * @return An equivalent {@link MediaMetadataCompat} object, or null if none. */ @Nullable @SuppressWarnings("deprecation") public static MediaDescriptionCompat fromMediaDescription(Object descriptionObj) { - if (descriptionObj != null) { + if (descriptionObj != null && Build.VERSION.SDK_INT >= 21) { Builder bob = new Builder(); MediaDescription description = (MediaDescription) descriptionObj; - bob.setMediaId(description.getMediaId()); - bob.setTitle(description.getTitle()); - bob.setSubtitle(description.getSubtitle()); - bob.setDescription(description.getDescription()); - bob.setIconBitmap(description.getIconBitmap()); - bob.setIconUri(description.getIconUri()); - Bundle extras = description.getExtras(); + bob.setMediaId(Api21Impl.getMediaId(description)); + bob.setTitle(Api21Impl.getTitle(description)); + bob.setSubtitle(Api21Impl.getSubtitle(description)); + bob.setDescription(Api21Impl.getDescription(description)); + bob.setIconBitmap(Api21Impl.getIconBitmap(description)); + bob.setIconUri(Api21Impl.getIconUri(description)); + Bundle extras = Api21Impl.getExtras(description); extras = MediaSessionCompat.unparcelWithClassLoader(extras); if (extras != null) { extras = new Bundle(extras); @@ -403,8 +419,12 @@ public final class MediaDescriptionCompat implements Parcelable { new Parcelable.Creator() { @Override public MediaDescriptionCompat createFromParcel(Parcel in) { - return checkNotNull( - fromMediaDescription(MediaDescription.CREATOR.createFromParcel(in))); + if (Build.VERSION.SDK_INT < 21) { + return new MediaDescriptionCompat(in); + } else { + return checkNotNull( + fromMediaDescription(MediaDescription.CREATOR.createFromParcel(in))); + } } @Override @@ -526,6 +546,99 @@ public final class MediaDescriptionCompat implements Parcelable { } } + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static MediaDescription.Builder createBuilder() { + return new MediaDescription.Builder(); + } + + @DoNotInline + static void setMediaId(MediaDescription.Builder builder, @Nullable String mediaId) { + builder.setMediaId(mediaId); + } + + @DoNotInline + static void setTitle(MediaDescription.Builder builder, @Nullable CharSequence title) { + builder.setTitle(title); + } + + @DoNotInline + static void setSubtitle(MediaDescription.Builder builder, @Nullable CharSequence subtitle) { + builder.setSubtitle(subtitle); + } + + @DoNotInline + static void setDescription( + MediaDescription.Builder builder, @Nullable CharSequence description) { + builder.setDescription(description); + } + + @DoNotInline + static void setIconBitmap(MediaDescription.Builder builder, @Nullable Bitmap icon) { + builder.setIconBitmap(icon); + } + + @DoNotInline + static void setIconUri(MediaDescription.Builder builder, @Nullable Uri iconUri) { + builder.setIconUri(iconUri); + } + + @DoNotInline + static void setExtras(MediaDescription.Builder builder, @Nullable Bundle extras) { + builder.setExtras(extras); + } + + @DoNotInline + static MediaDescription build(MediaDescription.Builder builder) { + return builder.build(); + } + + @DoNotInline + @Nullable + static String getMediaId(MediaDescription description) { + return description.getMediaId(); + } + + @DoNotInline + @Nullable + static CharSequence getTitle(MediaDescription description) { + return description.getTitle(); + } + + @DoNotInline + @Nullable + static CharSequence getSubtitle(MediaDescription description) { + return description.getSubtitle(); + } + + @DoNotInline + @Nullable + static CharSequence getDescription(MediaDescription description) { + return description.getDescription(); + } + + @DoNotInline + @Nullable + static Bitmap getIconBitmap(MediaDescription description) { + return description.getIconBitmap(); + } + + @DoNotInline + @Nullable + static Uri getIconUri(MediaDescription description) { + return description.getIconUri(); + } + + @DoNotInline + @Nullable + static Bundle getExtras(MediaDescription description) { + return description.getExtras(); + } + } + @RequiresApi(23) private static class Api23Impl { private Api23Impl() {} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java index 0bdcba9125..c4a38eed5c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java @@ -22,12 +22,14 @@ import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.media.MediaMetadata; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.StringDef; import androidx.collection.ArrayMap; @@ -555,12 +557,14 @@ public final class MediaMetadataCompat implements Parcelable { /** * Creates an instance from a framework {@link android.media.MediaMetadata} object. * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * * @param metadataObj A {@link android.media.MediaMetadata} object, or null if none. * @return An equivalent {@link MediaMetadataCompat} object, or null if none. */ @Nullable public static MediaMetadataCompat fromMediaMetadata(@Nullable Object metadataObj) { - if (metadataObj != null) { + if (metadataObj != null && Build.VERSION.SDK_INT >= 21) { Parcel p = Parcel.obtain(); ((MediaMetadata) metadataObj).writeToParcel(p, 0); p.setDataPosition(0); @@ -576,8 +580,11 @@ public final class MediaMetadataCompat implements Parcelable { /** * Gets the underlying framework {@link android.media.MediaMetadata} object. * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * * @return An equivalent {@link android.media.MediaMetadata} object, or null if none. */ + @RequiresApi(21) public Object getMediaMetadata() { if (mMetadataFwk == null) { Parcel p = Parcel.obtain(); diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java index f09a3bb4b8..228220210e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java @@ -29,11 +29,16 @@ 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; @@ -42,6 +47,7 @@ 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; @@ -55,6 +61,7 @@ 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.Nullable; @@ -324,6 +331,11 @@ public class MediaSessionCompat { // 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; @@ -351,19 +363,25 @@ public class MediaSessionCompat { *

The session will automatically be registered with the system but will not be published until * {@link #setActive(boolean) setActive(true)} is called. * + *

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. + * * @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 no longer used. + * 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. */ - @SuppressWarnings("unused") public MediaSessionCompat( Context context, String tag, @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent) { - this(context, tag, mbrComponent, null, null); + this(context, tag, mbrComponent, mbrIntent, null); } /** @@ -373,7 +391,10 @@ public class MediaSessionCompat { *

The session will automatically be registered with the system but will not be published until * {@link #setActive(boolean) setActive(true)} is called. * - *

The {@code sessionInfo} can include additional unchanging information about + *

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. 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. * @@ -381,20 +402,21 @@ public class MediaSessionCompat { * @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 no longer used + * 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. */ - @SuppressWarnings("unused") public MediaSessionCompat( Context context, String tag, @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent, @Nullable Bundle sessionInfo) { - this(context, tag, mbrComponent, null, sessionInfo, null /* session2Token */); + this(context, tag, mbrComponent, mbrIntent, sessionInfo, null /* session2Token */); } /** */ @@ -438,20 +460,26 @@ public class MediaSessionCompat { Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0); } - 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); + 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. + Looper myLooper = Looper.myLooper(); + Handler handler = new Handler(myLooper != null ? myLooper : Looper.getMainLooper()); + setCallback(new Callback() {}, handler); + mImpl.setMediaButtonReceiver(mbrIntent); } else { - mImpl = new MediaSessionImplApi21(context, tag, session2Token, sessionInfo); + mImpl = + new MediaSessionImplApi19( + context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); } - // Set default callback to respond to controllers' extra binder requests. - Looper myLooper = Looper.myLooper(); - Handler handler = new Handler(myLooper != null ? myLooper : Looper.getMainLooper()); - setCallback(new Callback() {}, handler); - mImpl.setMediaButtonReceiver(mbrIntent); mController = new MediaControllerCompat(context, this); if (sMaxBitmapSize == 0) { @@ -499,6 +527,7 @@ public class MediaSessionCompat { * @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, @Nullable Handler handler) { if (callback == null) { mImpl.setCallback(null, null); @@ -533,6 +562,9 @@ public class MediaSessionCompat { * 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. * + *

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) { @@ -567,6 +599,10 @@ public class MediaSessionCompat { * {@link #setPlaybackToLocal} was previously called that stream will stop receiving volume * changes for this session. * + *

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) { @@ -581,6 +617,9 @@ public class MediaSessionCompat { * 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. * + *

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) { @@ -627,6 +666,10 @@ public class MediaSessionCompat { * interacting with this session. The owner of the session is responsible for deciding how to * distribute these tokens. * + *

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() { @@ -772,6 +815,8 @@ public class MediaSessionCompat { /** * Gets the underlying framework {@link android.media.session.MediaSession} object. * + *

This method is only supported on API 21+. + * * @return The underlying {@link android.media.session.MediaSession} object, or null if none. */ @Nullable @@ -782,12 +827,12 @@ public class MediaSessionCompat { /** * Gets the underlying framework {@link android.media.RemoteControlClient} object. * - * @return The underlying {@link android.media.RemoteControlClient} object, or null if none. + *

This method is only supported on APIs 14-20. On API 21+ {@link #getMediaSession()} should be + * used instead. * - * @deprecated Use {@link #getMediaSession()} + * @return The underlying {@link android.media.RemoteControlClient} object, or null if none. */ @Nullable - @Deprecated public Object getRemoteControlClient() { return mImpl.getRemoteControlClient(); } @@ -855,6 +900,8 @@ public class MediaSessionCompat { /** * Creates an instance from a framework {@link android.media.session.MediaSession} object. * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * *

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 @@ -869,7 +916,7 @@ public class MediaSessionCompat { @Nullable public static MediaSessionCompat fromMediaSession( @Nullable Context context, @Nullable Object mediaSession) { - if (context == null || mediaSession == null) { + if (Build.VERSION.SDK_INT < 21 || context == null || mediaSession == null) { return null; } MediaSessionImpl impl; @@ -955,8 +1002,7 @@ public class MediaSessionCompat { */ public abstract static class Callback { final Object mLock = new Object(); - @androidx.annotation.NonNull - final MediaSession.Callback mCallbackFwk; + @Nullable final MediaSession.Callback mCallbackFwk; private boolean mMediaPlayPausePendingOnHandler; @GuardedBy("mLock") @@ -968,7 +1014,11 @@ public class MediaSessionCompat { CallbackHandler mCallbackHandler; public Callback() { - mCallbackFwk = new MediaSessionCallbackApi21(); + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackFwk = new MediaSessionCallbackApi21(); + } else { + mCallbackFwk = null; + } mSessionImpl = new WeakReference<>(null); } @@ -1303,10 +1353,12 @@ public class MediaSessionCompat { } } + @RequiresApi(21) private class MediaSessionCallbackApi21 extends MediaSession.Callback { MediaSessionCallbackApi21() {} @Override + @SuppressWarnings("deprecation") public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) { MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); if (sessionImpl == null) { @@ -1531,6 +1583,7 @@ public class MediaSessionCompat { } @Override + @SuppressWarnings("deprecation") public void onCustomAction(String action, @Nullable Bundle extras) { MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); if (sessionImpl == null) { @@ -1764,9 +1817,13 @@ public class MediaSessionCompat { * Creates a compat Token from a framework {@link android.media.session.MediaSession.Token} * object. * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and + * later. + * * @param token The framework token object. * @return A compat Token for use with {@link MediaControllerCompat}. */ + @RequiresApi(21) public static Token fromToken(Object token) { return fromToken(token, null); } @@ -1775,10 +1832,14 @@ public class MediaSessionCompat { * Creates a compat Token from a framework {@link android.media.session.MediaSession.Token} * object, and the extra binder. * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and + * later. + * * @param token The framework token object. * @param extraBinder The extra binder. * @return A compat Token for use with {@link MediaControllerCompat}. */ + @RequiresApi(21) /* package */ static Token fromToken(Object token, @Nullable IMediaSession extraBinder) { checkState(token != null); if (!(token instanceof MediaSession.Token)) { @@ -1794,7 +1855,11 @@ public class MediaSessionCompat { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable((Parcelable) mInner, flags); + if (android.os.Build.VERSION.SDK_INT >= 21) { + dest.writeParcelable((Parcelable) mInner, flags); + } else { + dest.writeStrongBinder((IBinder) mInner); + } } @Override @@ -1827,6 +1892,8 @@ public class MediaSessionCompat { /** * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. * + *

This method is only supported on API 21+. + * * @return The underlying {@link android.media.session.MediaSession.Token} object, or null if * none. */ @@ -1886,6 +1953,7 @@ public class MediaSessionCompat { * @param tokenBundle * @return A compat Token for use with {@link MediaControllerCompat}. */ + @SuppressWarnings("deprecation") @Nullable public static Token fromBundle(@Nullable Bundle tokenBundle) { if (tokenBundle == null) { @@ -1902,9 +1970,15 @@ public class MediaSessionCompat { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @SuppressWarnings("deprecation") @Override public Token createFromParcel(Parcel in) { - Object inner = in.readParcelable(null); + Object inner; + if (android.os.Build.VERSION.SDK_INT >= 21) { + inner = in.readParcelable(null); + } else { + inner = in.readStrongBinder(); + } return new Token(checkNotNull(inner)); } @@ -1984,15 +2058,17 @@ public class MediaSessionCompat { /** * Gets the underlying {@link android.media.session.MediaSession.QueueItem}. * - * @return The underlying {@link android.media.session.MediaSession.QueueItem}. + *

On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null is returned. + * + * @return The underlying {@link android.media.session.MediaSession.QueueItem} or null. */ - @androidx.annotation.NonNull + @Nullable public Object getQueueItem() { - if (mItemFwk != null) { + if (mItemFwk != null || android.os.Build.VERSION.SDK_INT < 21) { return mItemFwk; } mItemFwk = - new MediaSession.QueueItem((MediaDescription) mDescription.getMediaDescription(), mId); + Api21Impl.createQueueItem((MediaDescription) mDescription.getMediaDescription(), mId); return mItemFwk; } @@ -2000,15 +2076,18 @@ public class MediaSessionCompat { * Creates an instance from a framework {@link android.media.session.MediaSession.QueueItem} * object. * + *

This method is only supported on API 21+. + * * @param queueItem A {@link android.media.session.MediaSession.QueueItem} object. * @return An equivalent {@link QueueItem} object. */ + @RequiresApi(21) public static QueueItem fromQueueItem(Object queueItem) { MediaSession.QueueItem queueItemObj = (MediaSession.QueueItem) queueItem; - Object descriptionObj = queueItemObj.getDescription(); + Object descriptionObj = Api21Impl.getDescription(queueItemObj); MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription(descriptionObj); - long id = queueItemObj.getQueueId(); + long id = Api21Impl.getQueueId(queueItemObj); return new QueueItem(queueItemObj, description, id); } @@ -2016,13 +2095,15 @@ public class MediaSessionCompat { * Creates a list of {@link QueueItem} objects from a framework {@link * android.media.session.MediaSession.QueueItem} object list. * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * * @param itemList A list of {@link android.media.session.MediaSession.QueueItem} objects. * @return An equivalent list of {@link QueueItem} objects, or null if none. */ @Nullable public static List fromQueueItemList( @Nullable List itemList) { - if (itemList == null) { + if (itemList == null || Build.VERSION.SDK_INT < 21) { return null; } List items = new ArrayList<>(itemList.size()); @@ -2050,6 +2131,26 @@ public class MediaSessionCompat { 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(); + } + } } /** @@ -2160,6 +2261,1597 @@ public class MediaSessionCompat { 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; + @Nullable final Bundle mSessionInfo; + final AudioManager mAudioManager; + final RemoteControlClient mRcc; + + final Object mLock = new Object(); + final RemoteCallbackList mControllerCallbacks = + new RemoteCallbackList<>(); + + @Nullable private MessageHandler mHandler; + boolean mDestroyed = false; + boolean mIsActive = false; + @Nullable volatile Callback mCallback; + @Nullable private RemoteUserInfo mRemoteUserInfo; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + @Nullable + RegistrationCallbackHandler mRegistrationCallbackHandler; + + // For backward compatibility, these flags are always set. + @SessionFlags int mFlags = FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; + + @Nullable MediaMetadataCompat mMetadata; + @Nullable PlaybackStateCompat mState; + @Nullable PendingIntent mSessionActivity; + @Nullable List mQueue; + @Nullable CharSequence mQueueTitle; + @RatingCompat.Style int mRatingType; + boolean mCaptioningEnabled; + @PlaybackStateCompat.RepeatMode int mRepeatMode; + @PlaybackStateCompat.ShuffleMode int mShuffleMode; + @Nullable Bundle mExtras; + + int mVolumeType; + int mLocalStream; + @Nullable VolumeProviderCompat mVolumeProvider; + + private VolumeProviderCompat.Callback mVolumeCallback = + new VolumeProviderCompat.Callback() { + @SuppressWarnings("method.invocation.invalid") // referencing method from constructor + @Override + public void onVolumeChanged(VolumeProviderCompat volumeProvider) { + if (mVolumeProvider != volumeProvider) { + return; + } + ParcelableVolumeInfo info = + new ParcelableVolumeInfo( + mVolumeType, + mLocalStream, + volumeProvider.getVolumeControl(), + volumeProvider.getMaxVolume(), + volumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + } + }; + + @SuppressWarnings({ + "assignment.type.incompatible", + "argument.type.incompatible" + }) // Sharing this in constructor + public MediaSessionImplBase( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + if (mbrComponent == null) { + throw new IllegalArgumentException("MediaButtonReceiver component may not be null"); + } + mContext = context; + mSessionInfo = sessionInfo; + mAudioManager = (AudioManager) checkNotNull(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(@Nullable Callback callback, @Nullable 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, 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, @Nullable Object obj, @Nullable 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, @Nullable 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(@Nullable 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(checkNotNull(state)); + + // Set transport control flags + mRcc.setTransportControlFlags(getRccTransportControlFlagsFromActions(state.getActions())); + } + } + + @Nullable + @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(@Nullable 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", "argument.type.incompatible"}) + RemoteControlClient.MetadataEditor buildRccMetadata(@Nullable 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(@Nullable PendingIntent mbr) { + // Do nothing, changing this is not supported before API 21. + } + + @Override + public void setQueue(@Nullable List queue) { + mQueue = queue; + sendQueue(queue); + } + + @Override + public void setQueueTitle(CharSequence title) { + mQueueTitle = title; + sendQueueTitle(title); + } + + @Nullable + @Override + public Object getMediaSession() { + return null; + } + + @Nullable + @Override + public Object getRemoteControlClient() { + return null; + } + + @Nullable + @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(@Nullable Bundle extras) { + mExtras = extras; + sendExtras(extras); + } + + @Nullable + @Override + public RemoteUserInfo getCurrentControllerInfo() { + synchronized (mLock) { + return mRemoteUserInfo; + } + } + + @Override + public void setCurrentControllerInfo(@Nullable RemoteUserInfo remoteUserInfo) { + synchronized (mLock) { + mRemoteUserInfo = remoteUserInfo; + } + } + + @Nullable + @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, @Nullable 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(@Nullable 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(@Nullable 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(@Nullable List 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(@Nullable 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 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( + @Nullable String command, @Nullable Bundle args, @Nullable ResultReceiverWrapper cb) { + if (command == null) { + return; + } + postToHandler( + MessageHandler.MSG_COMMAND, + new Command(command, args, cb == null ? null : cb.mResultReceiver)); + } + + @Override + public boolean sendMediaButton(@Nullable KeyEvent mediaButton) { + postToHandler(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); + return true; + } + + @Override + public void registerCallbackListener(@Nullable IMediaControllerCallback cb) { + if (cb == null) { + return; + } + // 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(@Nullable IMediaControllerCallback cb) { + if (cb == null) { + return; + } + 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; + } + + @Nullable + @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; + } + + @Nullable + @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; + } + } + + @Nullable + @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) { + checkNotNull(vp); + 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, @Nullable String packageName) { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl != null) { + mediaSessionImpl.adjustVolume(direction, flags); + } + } + + @Override + public void setVolumeTo(int value, int flags, @Nullable 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(@Nullable String mediaId, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras); + } + + @Override + public void prepareFromSearch(@Nullable String query, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_SEARCH, query, extras); + } + + @Override + public void prepareFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_URI, uri, extras); + } + + @Override + public void play() throws RemoteException { + postToHandler(MessageHandler.MSG_PLAY); + } + + @Override + public void playFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); + } + + @Override + public void playFromSearch(@Nullable String query, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PLAY_SEARCH, query, extras); + } + + @Override + public void playFromUri(@Nullable Uri uri, @Nullable 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(@Nullable RatingCompat rating) { + postToHandler(MessageHandler.MSG_RATE, rating); + } + + @Override + public void rateWithExtras(@Nullable RatingCompat rating, @Nullable 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(@Nullable String action, @Nullable Bundle args) + throws RemoteException { + postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args); + } + + @Nullable + @Override + public MediaMetadataCompat getMetadata() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mMetadata : null; + } + + @Nullable + @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); + } + + @Nullable + @Override + public List getQueue() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + synchronized (mediaSessionImpl.mLock) { + return mediaSessionImpl.mQueue; + } + } + + @Override + public void addQueueItem(@Nullable MediaDescriptionCompat description) { + postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM, description); + } + + @Override + public void addQueueItemAt(@Nullable MediaDescriptionCompat description, int index) { + postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM_AT, description, index, /* extras= */ null); + } + + @Override + public void removeQueueItem(@Nullable MediaDescriptionCompat description) { + postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM, description); + } + + @Override + public void removeQueueItemAt(int index) { + postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM_AT, index); + } + + @Nullable + @Override + public CharSequence getQueueTitle() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mQueueTitle : null; + } + + @Nullable + @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, @Nullable Object obj) { + postToHandler(what, obj, /* arg1= */ 0, /* extras= */ null); + } + + void postToHandler(int what, @Nullable Object obj, @Nullable Bundle extras) { + postToHandler(what, obj, /* arg1= */ 0, extras); + } + + void postToHandler(int what, @Nullable Object obj, int arg1, @Nullable 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; + @Nullable public final Bundle extras; + @Nullable public final ResultReceiver stub; + + public Command(String command, @Nullable Bundle extras, @Nullable 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(@Nullable 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; + } + } + } + } + + static class MediaSessionImplApi18 extends MediaSessionImplBase { + private static boolean sIsMbrPendingIntentSupported = true; + + MediaSessionImplApi18( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); + } + + @SuppressWarnings("argument.type.incompatible") + @Override + public void setCallback(@Nullable Callback callback, @Nullable 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); + } + } + } + + static class MediaSessionImplApi19 extends MediaSessionImplApi18 { + MediaSessionImplApi19( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); + } + + @SuppressWarnings("argument.type.incompatible") + @Override + public void setCallback(@Nullable Callback callback, @Nullable 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", "argument.type.incompatible"}) + RemoteControlClient.MetadataEditor buildRccMetadata(@Nullable 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, + LegacyParcelableUtil.convert( + metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_RATING), + RatingCompat.CREATOR)); + } + 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, + LegacyParcelableUtil.convert( + metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_USER_RATING), + RatingCompat.CREATOR)); + } + return editor; + } + } + + @RequiresApi(21) static class MediaSessionImplApi21 implements MediaSessionImpl { final MediaSession mSessionFwk; final ExtraSession mExtraSession; @@ -2276,7 +3968,7 @@ public class MediaSessionCompat { @Override public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { - mSessionFwk.setPlaybackToRemote(volumeProvider.getVolumeProvider()); + mSessionFwk.setPlaybackToRemote((VolumeProvider) volumeProvider.getVolumeProvider()); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java index c9ad6d3fc6..48ed1fd936 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java @@ -71,8 +71,10 @@ public final class MediaSessionManager { private MediaSessionManager(Context context) { if (Build.VERSION.SDK_INT >= 28) { mImpl = new MediaSessionManagerImplApi28(context); - } else { + } else if (Build.VERSION.SDK_INT >= 21) { mImpl = new MediaSessionManagerImplApi21(context); + } else { + mImpl = new MediaSessionManagerImplBase(context); } } @@ -371,6 +373,7 @@ public final class MediaSessionManager { } } + @RequiresApi(21) private static class MediaSessionManagerImplApi21 extends MediaSessionManagerImplBase { MediaSessionManagerImplApi21(Context context) { super(context); diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java index 357ef3b62d..7395292926 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java @@ -840,14 +840,16 @@ public final class PlaybackStateCompat implements Parcelable { /** * Creates an instance from a framework {@link android.media.session.PlaybackState} object. * + *

This method is only supported on API 21+. + * * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none. * @return An equivalent {@link PlaybackStateCompat} object, or null if none. */ @Nullable public static PlaybackStateCompat fromPlaybackState(@Nullable Object stateObj) { - if (stateObj != null) { + if (stateObj != null && Build.VERSION.SDK_INT >= 21) { PlaybackState stateFwk = (PlaybackState) stateObj; - List customActionFwks = stateFwk.getCustomActions(); + List customActionFwks = Api21Impl.getCustomActions(stateFwk); List customActions = null; if (customActionFwks != null) { customActions = new ArrayList<>(customActionFwks.size()); @@ -867,16 +869,16 @@ public final class PlaybackStateCompat implements Parcelable { } PlaybackStateCompat stateCompat = new PlaybackStateCompat( - stateFwk.getState(), - stateFwk.getPosition(), - stateFwk.getBufferedPosition(), - stateFwk.getPlaybackSpeed(), - stateFwk.getActions(), + Api21Impl.getState(stateFwk), + Api21Impl.getPosition(stateFwk), + Api21Impl.getBufferedPosition(stateFwk), + Api21Impl.getPlaybackSpeed(stateFwk), + Api21Impl.getActions(stateFwk), ERROR_CODE_UNKNOWN_ERROR, - stateFwk.getErrorMessage(), - stateFwk.getLastPositionUpdateTime(), + Api21Impl.getErrorMessage(stateFwk), + Api21Impl.getLastPositionUpdateTime(stateFwk), customActions, - stateFwk.getActiveQueueItemId(), + Api21Impl.getActiveQueueItemId(stateFwk), extras); stateCompat.mStateFwk = stateFwk; return stateCompat; @@ -888,28 +890,30 @@ public final class PlaybackStateCompat implements Parcelable { /** * Gets the underlying framework {@link android.media.session.PlaybackState} object. * + *

This method is only supported on API 21+. + * * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none. */ @Nullable public Object getPlaybackState() { - if (mStateFwk == null) { - PlaybackState.Builder builder = new PlaybackState.Builder() - .setState(mState, mPosition, mSpeed, mUpdateTime) - .setBufferedPosition(mBufferedPosition) - .setActions(mActions) - .setErrorMessage(mErrorMessage); + if (mStateFwk == null && Build.VERSION.SDK_INT >= 21) { + PlaybackState.Builder builder = Api21Impl.createBuilder(); + Api21Impl.setState(builder, mState, mPosition, mSpeed, mUpdateTime); + Api21Impl.setBufferedPosition(builder, mBufferedPosition); + Api21Impl.setActions(builder, mActions); + Api21Impl.setErrorMessage(builder, mErrorMessage); for (PlaybackStateCompat.CustomAction customAction : mCustomActions) { PlaybackState.CustomAction action = (PlaybackState.CustomAction) customAction.getCustomAction(); if (action != null) { - builder.addCustomAction(action); + Api21Impl.addCustomAction(builder, action); } } - builder.setActiveQueueItemId(mActiveItemId); + Api21Impl.setActiveQueueItemId(builder, mActiveItemId); if (Build.VERSION.SDK_INT >= 22) { Api22Impl.setExtras(builder, mExtras); } - mStateFwk = builder.build(); + mStateFwk = Api21Impl.build(builder); } return mStateFwk; } @@ -972,19 +976,22 @@ public final class PlaybackStateCompat implements Parcelable { * Creates an instance from a framework {@link android.media.session.PlaybackState.CustomAction} * object. * + *

This method is only supported on API 21+. + * * @param customActionObj A {@link android.media.session.PlaybackState.CustomAction} object, or * null if none. * @return An equivalent {@link PlaybackStateCompat.CustomAction} object, or null if none. */ + @RequiresApi(21) public static PlaybackStateCompat.CustomAction fromCustomAction(Object customActionObj) { PlaybackState.CustomAction customActionFwk = (PlaybackState.CustomAction) customActionObj; - Bundle extras = customActionFwk.getExtras(); + Bundle extras = Api21Impl.getExtras(customActionFwk); MediaSessionCompat.ensureClassLoader(extras); PlaybackStateCompat.CustomAction customActionCompat = new PlaybackStateCompat.CustomAction( - customActionFwk.getAction(), - customActionFwk.getName(), - customActionFwk.getIcon(), + Api21Impl.getAction(customActionFwk), + Api21Impl.getName(customActionFwk), + Api21Impl.getIcon(customActionFwk), extras); customActionCompat.mCustomActionFwk = customActionFwk; return customActionCompat; @@ -994,18 +1001,21 @@ public final class PlaybackStateCompat implements Parcelable { * Gets the underlying framework {@link android.media.session.PlaybackState.CustomAction} * object. * + *

This method is only supported on API 21+. + * * @return An equivalent {@link android.media.session.PlaybackState.CustomAction} object, or * null if none. */ @Nullable public Object getCustomAction() { - if (mCustomActionFwk != null) { + if (mCustomActionFwk != null || Build.VERSION.SDK_INT < 21) { return mCustomActionFwk; } - return new PlaybackState.CustomAction.Builder(mAction, mName, mIcon) - .setExtras(mExtras) - .build(); + PlaybackState.CustomAction.Builder builder = + Api21Impl.createCustomActionBuilder(mAction, mName, mIcon); + Api21Impl.setExtras(builder, mExtras); + return Api21Impl.build(builder); } public static final Parcelable.Creator CREATOR = @@ -1391,6 +1401,142 @@ public final class PlaybackStateCompat implements Parcelable { } } + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static PlaybackState.Builder createBuilder() { + return new PlaybackState.Builder(); + } + + @DoNotInline + static void setState( + PlaybackState.Builder builder, + int state, + long position, + float playbackSpeed, + long updateTime) { + builder.setState(state, position, playbackSpeed, updateTime); + } + + @DoNotInline + static void setBufferedPosition(PlaybackState.Builder builder, long bufferedPosition) { + builder.setBufferedPosition(bufferedPosition); + } + + @DoNotInline + static void setActions(PlaybackState.Builder builder, long actions) { + builder.setActions(actions); + } + + @SuppressWarnings("argument.type.incompatible") // Platform class not annotated as nullable + @DoNotInline + static void setErrorMessage(PlaybackState.Builder builder, @Nullable CharSequence error) { + builder.setErrorMessage(error); + } + + @DoNotInline + static void addCustomAction( + PlaybackState.Builder builder, PlaybackState.CustomAction customAction) { + builder.addCustomAction(customAction); + } + + @DoNotInline + static void setActiveQueueItemId(PlaybackState.Builder builder, long id) { + builder.setActiveQueueItemId(id); + } + + @DoNotInline + static List getCustomActions(PlaybackState state) { + return state.getCustomActions(); + } + + @DoNotInline + static PlaybackState build(PlaybackState.Builder builder) { + return builder.build(); + } + + @DoNotInline + static int getState(PlaybackState state) { + return state.getState(); + } + + @DoNotInline + static long getPosition(PlaybackState state) { + return state.getPosition(); + } + + @DoNotInline + static long getBufferedPosition(PlaybackState state) { + return state.getBufferedPosition(); + } + + @DoNotInline + static float getPlaybackSpeed(PlaybackState state) { + return state.getPlaybackSpeed(); + } + + @DoNotInline + static long getActions(PlaybackState state) { + return state.getActions(); + } + + @Nullable + @DoNotInline + static CharSequence getErrorMessage(PlaybackState state) { + return state.getErrorMessage(); + } + + @DoNotInline + static long getLastPositionUpdateTime(PlaybackState state) { + return state.getLastPositionUpdateTime(); + } + + @DoNotInline + static long getActiveQueueItemId(PlaybackState state) { + return state.getActiveQueueItemId(); + } + + @DoNotInline + static PlaybackState.CustomAction.Builder createCustomActionBuilder( + String action, CharSequence name, int icon) { + return new PlaybackState.CustomAction.Builder(action, name, icon); + } + + @SuppressWarnings("argument.type.incompatible") // Platform class not annotated as nullable + @DoNotInline + static void setExtras(PlaybackState.CustomAction.Builder builder, @Nullable Bundle extras) { + builder.setExtras(extras); + } + + @DoNotInline + static PlaybackState.CustomAction build(PlaybackState.CustomAction.Builder builder) { + return builder.build(); + } + + @Nullable + @DoNotInline + static Bundle getExtras(PlaybackState.CustomAction customAction) { + return customAction.getExtras(); + } + + @DoNotInline + static String getAction(PlaybackState.CustomAction customAction) { + return customAction.getAction(); + } + + @DoNotInline + static CharSequence getName(PlaybackState.CustomAction customAction) { + return customAction.getName(); + } + + @DoNotInline + static int getIcon(PlaybackState.CustomAction customAction) { + return customAction.getIcon(); + } + } + @RequiresApi(22) private static class Api22Impl { private Api22Impl() {} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java index 663fbed659..dabbd761aa 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java @@ -323,6 +323,8 @@ public final class RatingCompat implements Parcelable { /** * Creates an instance from a framework {@link android.media.Rating} object. * + *

This method is only supported on API 19+. + * * @param ratingObj A {@link android.media.Rating} object, or null if none. * @return An equivalent {@link RatingCompat} object, or null if none. */ @@ -364,6 +366,8 @@ public final class RatingCompat implements Parcelable { /** * Gets the underlying framework {@link android.media.Rating} object. * + *

This method is only supported on API 19+. + * * @return An equivalent {@link android.media.Rating} object, or null if none. */ @Nullable diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java index 4c72becbd0..5f4cf6ce4b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java @@ -19,8 +19,10 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY; import android.media.VolumeProvider; import android.os.Build; +import androidx.annotation.DoNotInline; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.media3.common.util.UnstableApi; import java.lang.annotation.Retention; @@ -59,6 +61,7 @@ public abstract class VolumeProviderCompat { private final int mMaxVolume; @Nullable private final String mControlId; private int mCurrentVolume; + @Nullable private Callback mCallback; @Nullable private VolumeProvider mVolumeProviderFwk; @@ -129,7 +132,13 @@ public abstract class VolumeProviderCompat { */ public final void setCurrentVolume(int currentVolume) { mCurrentVolume = currentVolume; - getVolumeProvider().setCurrentVolume(currentVolume); + if (Build.VERSION.SDK_INT >= 21) { + VolumeProvider volumeProviderFwk = (VolumeProvider) getVolumeProvider(); + Api21Impl.setCurrentVolume(volumeProviderFwk, currentVolume); + } + if (mCallback != null) { + mCallback.onVolumeChanged(this); + } } /** @@ -157,12 +166,24 @@ public abstract class VolumeProviderCompat { */ public void onAdjustVolume(int direction) {} + /** + * Sets a callback to receive volume changes. + * + *

Used internally by the support library. + */ + public void setCallback(@Nullable Callback callback) { + mCallback = callback; + } + /** * Gets the underlying framework {@link android.media.VolumeProvider} object. * + *

This method is only supported on API 21+. + * * @return An equivalent {@link android.media.VolumeProvider} object, or null if none. */ - public VolumeProvider getVolumeProvider() { + @RequiresApi(21) + public Object getVolumeProvider() { if (mVolumeProviderFwk == null) { if (Build.VERSION.SDK_INT >= 30) { mVolumeProviderFwk = @@ -194,4 +215,19 @@ public abstract class VolumeProviderCompat { } return mVolumeProviderFwk; } + + /** Listens for changes to the volume. */ + public abstract static class Callback { + public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider); + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static void setCurrentVolume(VolumeProvider volumeProvider, int currentVolume) { + volumeProvider.setCurrentVolume(currentVolume); + } + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java index ac7005bdf3..4d01628dd8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java @@ -21,7 +21,9 @@ import static org.hamcrest.core.IsNot.not; import android.media.AudioAttributes; import android.media.AudioManager; +import android.os.Build; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -50,7 +52,11 @@ public class AudioAttributesCompatTest { AudioAttributesCompat mNotificationLegacyAAC; @Before + @SdkSuppress(minSdkVersion = 21) public void setUpApi21() { + if (Build.VERSION.SDK_INT < 21) { + return; + } mMediaAA = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) @@ -74,6 +80,7 @@ public class AudioAttributesCompatTest { } @Test + @SdkSuppress(minSdkVersion = 21) public void testCreateWithAudioAttributesApi21() { assertThat(mMediaAACFromAA, not(equalTo(null))); assertThat((AudioAttributes) mMediaAACFromAA.unwrap(), equalTo(mMediaAA)); @@ -83,6 +90,7 @@ public class AudioAttributesCompatTest { } @Test + @SdkSuppress(minSdkVersion = 21) public void testEqualityApi21() { assertThat("self equality", mMediaAACFromAA, equalTo(mMediaAACFromAA)); assertThat("different things", mMediaAACFromAA, not(equalTo(mNotificationAAC))); @@ -118,6 +126,7 @@ public class AudioAttributesCompatTest { } @Test + @SdkSuppress(minSdkVersion = 21) public void testLegacyStreamTypeInferenceApi21() { assertThat(mMediaAACFromAA.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC)); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java index 4d0483bf58..5c7e18c4df 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java @@ -22,6 +22,7 @@ import android.net.Uri; import android.os.Bundle; import androidx.media3.test.session.common.TestUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +30,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class MediaDescriptionCompatTest { + @SdkSuppress(minSdkVersion = 21) @Test public void roundTripViaFrameworkObject_returnsEqualMediaUriAndExtras() { Uri mediaUri = Uri.parse("androidx://media/uri"); @@ -52,6 +54,7 @@ public class MediaDescriptionCompatTest { TestUtils.equals(createExtras(), restoredDescription2.getExtras()); } + @SdkSuppress(minSdkVersion = 21) @Test public void getMediaDescription_withMediaUri_doesNotTouchExtras() { MediaDescriptionCompat originalDescription =