diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java index 2c679ab0d9..fa8724db05 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java @@ -38,6 +38,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.Executor; +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Browses media content offered by a {@link MediaLibraryService} in addition to the {@link @@ -45,12 +48,6 @@ import java.util.concurrent.Executor; */ public final class MediaBrowser extends MediaController { - private static final String WRONG_THREAD_ERROR_MESSAGE = - "MediaBrowser method is called from a wrong thread." - + " See javadoc of MediaController for details."; - - private final MediaBrowserImpl impl; - /** A builder for {@link MediaBrowser}. */ public static final class Builder { @@ -201,6 +198,12 @@ public final class MediaBrowser extends MediaController { @Nullable LibraryParams params) {} } + private static final String WRONG_THREAD_ERROR_MESSAGE = + "MediaBrowser method is called from a wrong thread." + + " See javadoc of MediaController for details."; + + @NotOnlyInitialized private @MonotonicNonNull MediaBrowserImpl impl; + /** Creates an instance from the {@link SessionToken}. */ /* package */ MediaBrowser( Context context, @@ -210,17 +213,24 @@ public final class MediaBrowser extends MediaController { Looper applicationLooper, ConnectionCallback connectionCallback) { super(context, token, connectionHints, listener, applicationLooper, connectionCallback); - this.impl = (MediaBrowserImpl) super.impl; } @Override - /* package */ MediaBrowserImpl createImpl( - Context context, MediaController thisRef, SessionToken token, Bundle connectionHints) { + /* package */ @UnderInitialization + MediaBrowserImpl createImpl( + @UnderInitialization MediaBrowser this, + Context context, + SessionToken token, + Bundle connectionHints, + Looper applicationLooper) { + MediaBrowserImpl impl; if (token.isLegacySession()) { - return new MediaBrowserImplLegacy(context, (MediaBrowser) thisRef, token); + impl = new MediaBrowserImplLegacy(context, this, token, applicationLooper); } else { - return new MediaBrowserImplBase(context, thisRef, token, connectionHints); + impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper); } + this.impl = impl; + return impl; } /** @@ -234,7 +244,7 @@ public final class MediaBrowser extends MediaController { public ListenableFuture> getLibraryRoot(@Nullable LibraryParams params) { verifyApplicationThread(); if (isConnected()) { - return impl.getLibraryRoot(params); + return checkNotNull(impl).getLibraryRoot(params); } return createDisconnectedFuture(); } @@ -254,7 +264,7 @@ public final class MediaBrowser extends MediaController { verifyApplicationThread(); checkNotEmpty(parentId, "parentId must not be empty"); if (isConnected()) { - return impl.subscribe(parentId, params); + return checkNotNull(impl).subscribe(parentId, params); } return createDisconnectedFuture(); } @@ -273,7 +283,7 @@ public final class MediaBrowser extends MediaController { verifyApplicationThread(); checkNotEmpty(parentId, "parentId must not be empty"); if (isConnected()) { - return impl.unsubscribe(parentId); + return checkNotNull(impl).unsubscribe(parentId); } return createDisconnectedFuture(); } @@ -299,7 +309,7 @@ public final class MediaBrowser extends MediaController { checkArgument(page >= 0, "page must not be negative"); checkArgument(pageSize >= 1, "pageSize must not be less than 1"); if (isConnected()) { - return impl.getChildren(parentId, page, pageSize, params); + return checkNotNull(impl).getChildren(parentId, page, pageSize, params); } return createDisconnectedFuture(); } @@ -316,7 +326,7 @@ public final class MediaBrowser extends MediaController { verifyApplicationThread(); checkNotEmpty(mediaId, "mediaId must not be empty"); if (isConnected()) { - return impl.getItem(mediaId); + return checkNotNull(impl).getItem(mediaId); } return createDisconnectedFuture(); } @@ -338,7 +348,7 @@ public final class MediaBrowser extends MediaController { verifyApplicationThread(); checkNotEmpty(query, "query must not be empty"); if (isConnected()) { - return impl.search(query, params); + return checkNotNull(impl).search(query, params); } return createDisconnectedFuture(); } @@ -365,7 +375,7 @@ public final class MediaBrowser extends MediaController { checkArgument(page >= 0, "page must not be negative"); checkArgument(pageSize >= 1, "pageSize must not be less than 1"); if (isConnected()) { - return impl.getSearchResult(query, page, pageSize, params); + return checkNotNull(impl).getSearchResult(query, page, pageSize, params); } return createDisconnectedFuture(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java index f5ca3540f8..441e71c7a7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java @@ -28,6 +28,7 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBS import android.content.Context; import android.os.Bundle; +import android.os.Looper; import android.os.RemoteException; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; @@ -37,18 +38,27 @@ import androidx.media3.session.SequencedFutureManager.SequencedFuture; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.checkerframework.checker.initialization.qual.UnderInitialization; /** Base implementation of MediaBrowser. */ /* package */ class MediaBrowserImplBase extends MediaControllerImplBase implements MediaBrowser.MediaBrowserImpl { + private final MediaBrowser instance; + MediaBrowserImplBase( - Context context, MediaController instance, SessionToken token, Bundle connectionHints) { - super(context, instance, token, connectionHints); + Context context, + @UnderInitialization MediaBrowser instance, + SessionToken token, + Bundle connectionHints, + Looper applicationLooper) { + super(context, instance, token, connectionHints, applicationLooper); + this.instance = instance; } - MediaBrowser getMediaBrowser() { - return (MediaBrowser) instance; + @Override + /* package */ MediaBrowser getInstance() { + return instance; } @Override @@ -157,10 +167,10 @@ import com.google.common.util.concurrent.ListenableFuture; if (!isConnected()) { return; } - getMediaBrowser() + getInstance() .notifyBrowserListener( listener -> - listener.onSearchResultChanged(getMediaBrowser(), query, itemCount, libraryParams)); + listener.onSearchResultChanged(getInstance(), query, itemCount, libraryParams)); } void notifyChildrenChanged( @@ -168,10 +178,10 @@ import com.google.common.util.concurrent.ListenableFuture; if (!isConnected()) { return; } - getMediaBrowser() + getInstance() .notifyBrowserListener( listener -> - listener.onChildrenChanged(getMediaBrowser(), parentId, itemCount, libraryParams)); + listener.onChildrenChanged(getInstance(), parentId, itemCount, libraryParams)); } private ListenableFuture> dispatchRemoteLibrarySessionTask( diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java index b5e758846e..742c31e879 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -22,6 +22,7 @@ import static androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN; import android.content.Context; import android.os.Bundle; +import android.os.Looper; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserCompat.ItemCallback; import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; @@ -38,6 +39,7 @@ import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import org.checkerframework.checker.initialization.qual.UnderInitialization; /** Implementation of MediaBrowser with the {@link MediaBrowserCompat} for legacy support. */ /* package */ class MediaBrowserImplLegacy extends MediaControllerImplLegacy @@ -49,12 +51,20 @@ import java.util.List; private final HashMap> subscribeCallbacks = new HashMap<>(); - MediaBrowserImplLegacy(Context context, MediaBrowser instance, SessionToken token) { - super(context, instance, token); + private final MediaBrowser instance; + + MediaBrowserImplLegacy( + Context context, + @UnderInitialization MediaBrowser instance, + SessionToken token, + Looper applicationLooper) { + super(context, instance, token, applicationLooper); + this.instance = instance; } - MediaBrowser getMediaBrowser() { - return (MediaBrowser) instance; + @Override + /* package*/ MediaBrowser getInstance() { + return instance; } @Override @@ -78,7 +88,8 @@ import java.util.List; @Override public ListenableFuture> getLibraryRoot(@Nullable LibraryParams params) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)) { + if (!getInstance() + .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } SettableFuture> result = SettableFuture.create(); @@ -103,7 +114,7 @@ import java.util.List; @Override public ListenableFuture> subscribe( String parentId, @Nullable LibraryParams params) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE)) { + if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -124,7 +135,7 @@ import java.util.List; @Override public ListenableFuture> unsubscribe(String parentId) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE)) { + if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -148,7 +159,8 @@ import java.util.List; @Override public ListenableFuture>> getChildren( String parentId, int page, int pageSize, @Nullable LibraryParams params) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN)) { + if (!getInstance() + .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -164,7 +176,7 @@ import java.util.List; @Override public ListenableFuture> getItem(String mediaId) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)) { + if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -196,7 +208,7 @@ import java.util.List; @Override public ListenableFuture> search( String query, @Nullable LibraryParams params) { - if (!instance.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH)) { + if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -210,7 +222,7 @@ import java.util.List; @Override public void onSearchResult( String query, Bundle extras, List items) { - getMediaBrowser() + getInstance() .notifyBrowserListener( listener -> { // Set extra null here, because 'extra' have different meanings between old @@ -218,20 +230,20 @@ import java.util.List; // - Old API: Extra/Option specified with search(). // - New API: Extra from MediaLibraryService to MediaBrowser // TODO(b/193193565): Cache search result for later getSearchResult() calls. - listener.onSearchResultChanged(getMediaBrowser(), query, items.size(), null); + listener.onSearchResultChanged(getInstance(), query, items.size(), null); }); } @Override public void onError(String query, Bundle extras) { - getMediaBrowser() + getInstance() .notifyBrowserListener( listener -> { // Set extra null here, because 'extra' have different meanings between old // API and new API as follows. // - Old API: Extra/Option specified with search(). // - New API: Extra from MediaLibraryService to MediaBrowser - listener.onSearchResultChanged(getMediaBrowser(), query, 0, null); + listener.onSearchResultChanged(getInstance(), query, 0, null); }); } }); @@ -242,8 +254,8 @@ import java.util.List; @Override public ListenableFuture>> getSearchResult( String query, int page, int pageSize, @Nullable LibraryParams params) { - if (!instance.isSessionCommandAvailable( - SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT)) { + if (!getInstance() + .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT)) { return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); @@ -401,11 +413,11 @@ import java.util.List; LibraryParams params = MediaUtils.convertToLibraryParams( context, browserCompat.getNotifyChildrenChangedOptions()); - getMediaBrowser() + getInstance() .notifyBrowserListener( listener -> { // TODO(b/193193565): Cache children result for later getChildren() calls. - listener.onChildrenChanged(getMediaBrowser(), parentId, itemCount, params); + listener.onChildrenChanged(getInstance(), parentId, itemCount, params); }); future.set(LibraryResult.ofVoid()); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 7b474770a3..7e51ee2d17 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -64,7 +64,8 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; -import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; /** * A controller that interacts with a {@link MediaSession}, a {@link MediaSessionService} hosting a @@ -375,7 +376,7 @@ public class MediaController implements Player { private boolean released; - /* package */ final MediaControllerImpl impl; + @NotOnlyInitialized private final MediaControllerImpl impl; /* package */ final Listener listener; @@ -407,19 +408,21 @@ public class MediaController implements Player { applicationHandler = new Handler(applicationLooper); this.connectionCallback = connectionCallback; - @SuppressWarnings("nullness:assignment") - @Initialized - MediaController thisRef = this; - impl = thisRef.createImpl(context, thisRef, token, connectionHints); + impl = createImpl(context, token, connectionHints, applicationLooper); impl.connect(); } - /* package */ MediaControllerImpl createImpl( - Context context, MediaController thisRef, SessionToken token, Bundle connectionHints) { + /* package */ @UnderInitialization + MediaControllerImpl createImpl( + @UnderInitialization MediaController this, + Context context, + SessionToken token, + Bundle connectionHints, + Looper applicationLooper) { if (token.isLegacySession()) { - return new MediaControllerImplLegacy(context, thisRef, token); + return new MediaControllerImplLegacy(context, this, token, applicationLooper); } else { - return new MediaControllerImplBase(context, thisRef, token, connectionHints); + return new MediaControllerImplBase(context, this, token, connectionHints, applicationLooper); } } @@ -1805,7 +1808,7 @@ public class MediaController implements Player { interface MediaControllerImpl { - void connect(); + void connect(@UnderInitialization MediaControllerImpl this); void addListener(Player.Listener listener); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index e27d40cd5c..46652a4036 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -154,6 +154,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.qual.NonNull; @SuppressWarnings("FutureReturnValueIgnored") // TODO(b/138091975): Not to ignore if feasible @@ -161,7 +162,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; public static final String TAG = "MCImplBase"; - protected final MediaController instance; + private final MediaController instance; protected final SequencedFutureManager sequencedFutureManager; protected final MediaControllerStub controllerStub; @@ -192,7 +193,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; private long lastSetPlayWhenReadyCalledTimeMs; public MediaControllerImplBase( - Context context, MediaController instance, SessionToken token, Bundle connectionHints) { + Context context, + @UnderInitialization MediaController instance, + SessionToken token, + Bundle connectionHints, + Looper applicationLooper) { // Initialize default values. playerInfo = PlayerInfo.DEFAULT; sessionCommands = SessionCommands.EMPTY; @@ -201,9 +206,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; intersectedPlayerCommands = Commands.EMPTY; listeners = new ListenerSet<>( - instance.getApplicationLooper(), + applicationLooper, Clock.DEFAULT, - (listener, flags) -> listener.onEvents(instance, new Events(flags))); + (listener, flags) -> listener.onEvents(getInstance(), new Events(flags))); // Initialize members this.instance = instance; @@ -216,21 +221,26 @@ import org.checkerframework.checker.nullness.qual.NonNull; this.connectionHints = connectionHints; deathRecipient = () -> - MediaControllerImplBase.this.instance.runOnApplicationLooper( - MediaControllerImplBase.this.instance::release); + MediaControllerImplBase.this + .getInstance() + .runOnApplicationLooper(MediaControllerImplBase.this.getInstance()::release); surfaceCallback = new SurfaceCallback(); serviceConnection = (this.token.getType() == TYPE_SESSION) ? null : new SessionServiceConnection(connectionHints); - flushCommandQueueHandler = new FlushCommandQueueHandler(instance.getApplicationLooper()); + flushCommandQueueHandler = new FlushCommandQueueHandler(applicationLooper); lastReturnedContentPositionMs = C.TIME_UNSET; lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET; } + /* package*/ MediaController getInstance() { + return instance; + } + @Override - public void connect() { + public void connect(@UnderInitialization MediaControllerImplBase this) { boolean connectionRequested; if (this.token.getType() == TYPE_SESSION) { // Session @@ -241,7 +251,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; connectionRequested = requestConnectToService(); } if (!connectionRequested) { - this.instance.runOnApplicationLooper(MediaControllerImplBase.this.instance::release); + getInstance().runOnApplicationLooper(getInstance()::release); } } @@ -640,8 +650,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; return playerInfo.sessionPositionInfo.positionInfo.positionMs; } long elapsedTimeMs = - (instance.getTimeDiffMs() != C.TIME_UNSET) - ? instance.getTimeDiffMs() + (getInstance().getTimeDiffMs() != C.TIME_UNSET) + ? getInstance().getTimeDiffMs() : SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs; long estimatedPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs @@ -694,8 +704,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; } long elapsedTimeMs = - (instance.getTimeDiffMs() != C.TIME_UNSET) - ? instance.getTimeDiffMs() + (getInstance().getTimeDiffMs() != C.TIME_UNSET) + ? getInstance().getTimeDiffMs() : SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs; long estimatedPositionMs = playerInfo.sessionPositionInfo.positionInfo.contentPositionMs @@ -2289,7 +2299,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; TAG, "Cannot be notified about the connection result many times." + " Probably a bug or malicious app."); - instance.release(); + getInstance().release(); return; } iSession = result.sessionBinder; @@ -2304,7 +2314,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; // so can be used without worrying about deadlock. result.sessionBinder.asBinder().linkToDeath(deathRecipient, 0); } catch (RemoteException e) { - instance.release(); + getInstance().release(); return; } connectedToken = @@ -2315,7 +2325,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; token.getPackageName(), result.sessionBinder, result.tokenExtras); - instance.notifyAccepted(); + getInstance().notifyAccepted(); } private void sendControllerResult(int seq, SessionResult result) { @@ -2350,14 +2360,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isConnected()) { return; } - instance.notifyControllerListener( - listener -> { - ListenableFuture future = - checkNotNull( - listener.onCustomCommand(instance, command, args), - "ControllerCallback#onCustomCommand() must not return null"); - sendControllerResultWhenReady(seq, future); - }); + getInstance() + .notifyControllerListener( + listener -> { + ListenableFuture future = + checkNotNull( + listener.onCustomCommand(getInstance(), command, args), + "ControllerCallback#onCustomCommand() must not return null"); + sendControllerResultWhenReady(seq, future); + }); } @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. @@ -2558,8 +2569,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands)); } if (sessionCommandsChanged) { - instance.notifyControllerListener( - listener -> listener.onAvailableSessionCommandsChanged(instance, sessionCommands)); + getInstance() + .notifyControllerListener( + listener -> + listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands)); } } @@ -2596,21 +2609,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; validatedCustomLayout.add(button); } } - instance.notifyControllerListener( - listener -> { - ListenableFuture future = - checkNotNull( - listener.onSetCustomLayout(instance, validatedCustomLayout), - "MediaController.Listener#onSetCustomLayout() must not return null"); - sendControllerResultWhenReady(seq, future); - }); + getInstance() + .notifyControllerListener( + listener -> { + ListenableFuture future = + checkNotNull( + listener.onSetCustomLayout(getInstance(), validatedCustomLayout), + "MediaController.Listener#onSetCustomLayout() must not return null"); + sendControllerResultWhenReady(seq, future); + }); } public void onExtrasChanged(Bundle extras) { if (!isConnected()) { return; } - instance.notifyControllerListener(listener -> listener.onExtrasChanged(instance, extras)); + getInstance() + .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); } public void onRenderedFirstFrame() { @@ -2961,7 +2976,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; Log.w(TAG, "Service " + name + " has died prematurely"); } finally { if (!connectionRequested) { - instance.runOnApplicationLooper(instance::release); + getInstance().runOnApplicationLooper(getInstance()::release); } } } @@ -2972,7 +2987,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; // rebind, but we'd better to release() here. Otherwise ControllerCallback#onConnected() // would be called multiple times, and the controller would be connected to the // different session everytime. - instance.runOnApplicationLooper(instance::release); + getInstance().runOnApplicationLooper(getInstance()::release); } @Override @@ -2980,7 +2995,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; // Permanent lose of the binding because of the service package update or removed. // This SessionServiceRecord will be removed accordingly, but forget session binder here // for sure. - instance.runOnApplicationLooper(instance::release); + getInstance().runOnApplicationLooper(getInstance()::release); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 893c6ecd92..25489f6527 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -46,6 +46,7 @@ import android.content.Context; import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.ResultReceiver; import android.os.SystemClock; import android.support.v4.media.MediaBrowserCompat; @@ -98,6 +99,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Future; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.compatqual.NullableType; /* package */ class MediaControllerImplLegacy implements MediaController.MediaControllerImpl { @@ -110,7 +112,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; new CueGroup(ImmutableList.of(), /* presentationTimeUs= */ 0); /* package */ final Context context; - /* package */ final MediaController instance; + private final MediaController instance; private final SessionToken token; private final ListenerSet listeners; @@ -124,26 +126,34 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private LegacyPlayerInfo pendingLegacyPlayerInfo; private ControllerInfo controllerInfo; - public MediaControllerImplLegacy(Context context, MediaController instance, SessionToken token) { + public MediaControllerImplLegacy( + Context context, + @UnderInitialization MediaController instance, + SessionToken token, + Looper applicationLooper) { // Initialize default values. legacyPlayerInfo = new LegacyPlayerInfo(); pendingLegacyPlayerInfo = new LegacyPlayerInfo(); controllerInfo = new ControllerInfo(); listeners = new ListenerSet<>( - instance.getApplicationLooper(), + applicationLooper, Clock.DEFAULT, - (listener, flags) -> listener.onEvents(instance, new Events(flags))); + (listener, flags) -> listener.onEvents(getInstance(), new Events(flags))); // Initialize members. this.context = context; this.instance = instance; - controllerCompatCallback = new ControllerCompatCallback(); + controllerCompatCallback = new ControllerCompatCallback(applicationLooper); this.token = token; } + /* package */ MediaController getInstance() { + return instance; + } + @Override - public void connect() { + public void connect(@UnderInitialization MediaControllerImplLegacy this) { if (this.token.getType() == SessionToken.TYPE_SESSION) { connectToSession((MediaSessionCompat.Token) checkStateNotNull(this.token.getBinder())); } else { @@ -590,7 +600,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } SettableFuture result = SettableFuture.create(); ResultReceiver cb = - new ResultReceiver(instance.applicationHandler) { + new ResultReceiver(getInstance().applicationHandler) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { result.set( @@ -1233,36 +1243,43 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void connectToSession(MediaSessionCompat.Token sessionCompatToken) { - instance.runOnApplicationLooper( - () -> { - controllerCompat = new MediaControllerCompat(context, sessionCompatToken); - // Note: registerCallback() will invoke MediaControllerCompat.Callback#onSessionReady() - // if the controller is already ready. - controllerCompat.registerCallback(controllerCompatCallback, instance.applicationHandler); - }); + getInstance() + .runOnApplicationLooper( + () -> { + controllerCompat = new MediaControllerCompat(context, sessionCompatToken); + // Note: registerCallback() will invoke + // MediaControllerCompat.Callback#onSessionReady() + // if the controller is already ready. + controllerCompat.registerCallback( + controllerCompatCallback, getInstance().applicationHandler); + }); // Post a runnable to prevent callbacks from being called by onConnectedNotLocked() // before the constructor returns (b/196941334). - instance.applicationHandler.post( - () -> { - if (!controllerCompat.isSessionReady()) { - // If the session not ready here, then call onConnectedNotLocked() immediately. The - // session may be a framework MediaSession and we cannot know whether it can be ready - // later. - onConnectedNotLocked(); - } - }); + getInstance() + .applicationHandler + .post( + () -> { + if (!controllerCompat.isSessionReady()) { + // If the session not ready here, then call onConnectedNotLocked() immediately. The + // session may be a framework MediaSession and we cannot know whether it can be + // ready + // later. + onConnectedNotLocked(); + } + }); } private void connectToService() { - instance.runOnApplicationLooper( - () -> { - // BrowserCompat can only be used on the thread that it's created. - // Create it on the application looper to respect that. - browserCompat = - new MediaBrowserCompat( - context, token.getComponentName(), new ConnectionCallback(), null); - browserCompat.connect(); - }); + getInstance() + .runOnApplicationLooper( + () -> { + // BrowserCompat can only be used on the thread that it's created. + // Create it on the application looper to respect that. + browserCompat = + new MediaBrowserCompat( + context, token.getComponentName(), new ConnectionCallback(), null); + browserCompat.connect(); + }); } private boolean isPrepared() { @@ -1365,14 +1382,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; controllerCompat.getFlags(), controllerCompat.isSessionReady(), controllerCompat.getRatingType(), - instance.getTimeDiffMs()); + getInstance().getTimeDiffMs()); Pair<@NullableType Integer, @NullableType Integer> reasons = calculateDiscontinuityAndTransitionReason( legacyPlayerInfo, controllerInfo, newLegacyPlayerInfo, newControllerInfo, - instance.getTimeDiffMs()); + getInstance().getTimeDiffMs()); updateControllerInfo( notifyConnected, newLegacyPlayerInfo, @@ -1412,11 +1429,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; controllerInfo = newControllerInfo; if (notifyConnected) { - instance.notifyAccepted(); + getInstance().notifyAccepted(); if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { - instance.notifyControllerListener( - listener -> - ignoreFuture(listener.onSetCustomLayout(instance, newControllerInfo.customLayout))); + getInstance() + .notifyControllerListener( + listener -> + ignoreFuture( + listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout))); } return; } @@ -1537,15 +1556,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (!oldControllerInfo.availableSessionCommands.equals( newControllerInfo.availableSessionCommands)) { - instance.notifyControllerListener( - listener -> - listener.onAvailableSessionCommandsChanged( - instance, newControllerInfo.availableSessionCommands)); + getInstance() + .notifyControllerListener( + listener -> + listener.onAvailableSessionCommandsChanged( + getInstance(), newControllerInfo.availableSessionCommands)); } if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { - instance.notifyControllerListener( - listener -> - ignoreFuture(listener.onSetCustomLayout(instance, newControllerInfo.customLayout))); + getInstance() + .notifyControllerListener( + listener -> + ignoreFuture( + listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout))); } listeners.flushEvents(); } @@ -1566,12 +1588,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onConnectionSuspended() { - instance.release(); + getInstance().release(); } @Override public void onConnectionFailed() { - instance.release(); + getInstance().release(); } } @@ -1581,10 +1603,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Handler pendingChangesHandler; - public ControllerCompatCallback() { + public ControllerCompatCallback(Looper applicationLooper) { pendingChangesHandler = new Handler( - instance.applicationHandler.getLooper(), + applicationLooper, (msg) -> { if (msg.what == MSG_HANDLE_PENDING_UPDATES) { handleNewLegacyParameters(/* notifyConnected= */ false, pendingLegacyPlayerInfo); @@ -1620,16 +1642,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onSessionDestroyed() { - instance.release(); + getInstance().release(); } @Override public void onSessionEvent(String event, Bundle extras) { - instance.notifyControllerListener( - listener -> - ignoreFuture( - listener.onCustomCommand( - instance, new SessionCommand(event, /* extras= */ Bundle.EMPTY), extras))); + getInstance() + .notifyControllerListener( + listener -> + ignoreFuture( + listener.onCustomCommand( + getInstance(), + new SessionCommand(event, /* extras= */ Bundle.EMPTY), + extras))); } @Override @@ -1661,7 +1686,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onExtrasChanged(Bundle extras) { - instance.notifyControllerListener(listener -> listener.onExtrasChanged(instance, extras)); + getInstance() + .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); } @Override @@ -1672,17 +1698,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onCaptioningEnabledChanged(boolean enabled) { - instance.notifyControllerListener( - listener -> { - Bundle args = new Bundle(); - args.putBoolean(ARGUMENT_CAPTIONING_ENABLED, enabled); - ignoreFuture( - listener.onCustomCommand( - instance, - new SessionCommand( - SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED, /* extras= */ Bundle.EMPTY), - args)); - }); + getInstance() + .notifyControllerListener( + listener -> { + Bundle args = new Bundle(); + args.putBoolean(ARGUMENT_CAPTIONING_ENABLED, enabled); + ignoreFuture( + listener.onCustomCommand( + getInstance(), + new SessionCommand( + SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED, + /* extras= */ Bundle.EMPTY), + args)); + }); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java index 9d86ac05be..1e27fd8e65 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java @@ -87,7 +87,8 @@ import java.util.List; @Override public void onDisconnected(int seq) { dispatchControllerTaskOnHandler( - controller -> controller.instance.runOnApplicationLooper(controller.instance::release)); + controller -> + controller.getInstance().runOnApplicationLooper(controller.getInstance()::release)); } @Override @@ -268,7 +269,7 @@ import java.util.List; if (controller == null) { return; } - Handler handler = controller.instance.applicationHandler; + Handler handler = controller.getInstance().applicationHandler; postOrRun( handler, () -> {