Add MediaSession.setSessionActivity(PendingIntent)

The session activity is already sent to the controller with the
`ConnectionState` when it connects. This change adds the ability to update the
activity.

This allows an app to change the intent that is used to open an activity
for the notification. An app is likely to want to change the session activity
just before the session is released. This allows to use a different activity or
more importantly the back stack of the activity for while the app is running
and when used for the playback resumption notification.

PiperOrigin-RevId: 530627102
This commit is contained in:
bachinger 2023-05-09 16:13:53 +00:00 committed by Tofunmi Adigun-Hameed
parent 1105f194ca
commit 5e4421c2ba
13 changed files with 159 additions and 7 deletions

View file

@ -16,6 +16,7 @@
package androidx.media3.session;
import android.os.Bundle;
import android.app.PendingIntent;
import androidx.media3.session.IMediaSession;
/**
@ -46,7 +47,8 @@ oneway interface IMediaController {
int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009;
void onRenderedFirstFrame(int seq) = 3010;
void onExtrasChanged(int seq, in Bundle extras) = 3011;
// Next Id for MediaController: 3013
void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013;
// Next Id for MediaController: 3014
void onChildrenChanged(
int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000;

View file

@ -396,6 +396,17 @@ public class MediaController implements Player {
* @param extras The session extras that have changed.
*/
default void onExtrasChanged(MediaController controller, Bundle extras) {}
/**
* Called when the {@link PendingIntent} to launch the session activity {@link
* MediaSession#setSessionActivity(PendingIntent) has been changed} on the session side.
*
* @param controller The controller.
* @param sessionActivity The pending intent to launch the session activity.
*/
@UnstableApi
default void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) {}
}
/* package */ interface ConnectionCallback {

View file

@ -2595,6 +2595,16 @@ import org.checkerframework.checker.nullness.qual.NonNull;
.notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras));
}
void onSetSessionActivity(int seq, PendingIntent sessionActivity) {
if (!isConnected()) {
return;
}
this.sessionActivity = sessionActivity;
getInstance()
.notifyControllerListener(
listener -> listener.onSessionActivityChanged(getInstance(), sessionActivity));
}
public void onRenderedFirstFrame() {
listeners.sendEvent(
/* eventFlag= */ Player.EVENT_RENDERED_FIRST_FRAME, Listener::onRenderedFirstFrame);

View file

@ -17,9 +17,11 @@ package androidx.media3.session;
import static androidx.media3.common.util.Util.postOrRun;
import android.app.PendingIntent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.media3.common.Player.Commands;
@ -36,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
private static final String TAG = "MediaControllerStub";
/** The version of the IMediaController interface. */
public static final int VERSION_INT = 2;
public static final int VERSION_INT = 3;
private final WeakReference<MediaControllerImplBase> controller;
@ -157,6 +159,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchControllerTaskOnHandler(controller -> controller.onCustomCommand(seq, command, args));
}
@Override
public void onSessionActivityChanged(int seq, PendingIntent sessionActivity)
throws RemoteException {
dispatchControllerTaskOnHandler(
controller -> controller.onSetSessionActivity(seq, sessionActivity));
}
@Override
public void onPeriodicSessionPositionInfoChanged(int seq, Bundle sessionPositionInfoBundle) {
SessionPositionInfo sessionPositionInfo;

View file

@ -599,6 +599,17 @@ public class MediaSession {
return impl.getSessionActivity();
}
/**
* Updates the session activity that was set when {@linkplain
* Builder#setSessionActivity(PendingIntent) building the session}.
*
* @param activityPendingIntent The pending intent to start the session activity.
*/
@UnstableApi
public final void setSessionActivity(PendingIntent activityPendingIntent) {
impl.setSessionActivity(activityPendingIntent);
}
/**
* Sets the underlying {@link Player} for this session to dispatch incoming events to.
*
@ -1382,6 +1393,9 @@ public class MediaSession {
default void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {}
default void onSessionActivityChanged(int seq, PendingIntent sessionActivity)
throws RemoteException {}
default void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {}
default void sendCustomCommand(int seq, SessionCommand command, Bundle args)

View file

@ -66,6 +66,7 @@ import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
@ -80,9 +81,11 @@ import com.google.common.util.concurrent.SettableFuture;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ class MediaSessionImpl {
@ -119,18 +122,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final String sessionId;
private final SessionToken sessionToken;
private final MediaSession instance;
@Nullable private final PendingIntent sessionActivity;
private final Handler applicationHandler;
private final BitmapLoader bitmapLoader;
private final Runnable periodicSessionPositionInfoUpdateRunnable;
private final Handler mainHandler;
@Nullable private PlayerListener playerListener;
@Nullable private MediaSession.Listener mediaSessionListener;
private PlayerInfo playerInfo;
private PlayerWrapper playerWrapper;
private @MonotonicNonNull PendingIntent sessionActivity;
@Nullable private PlayerListener playerListener;
@Nullable private MediaSession.Listener mediaSessionListener;
@Nullable private ControllerInfo controllerForCurrentRequest;
@GuardedBy("lock")
@ -531,6 +532,25 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return sessionActivity;
}
@UnstableApi
protected void setSessionActivity(PendingIntent sessionActivity) {
if (Objects.equals(this.sessionActivity, sessionActivity)) {
return;
}
this.sessionActivity = sessionActivity;
sessionLegacyStub.getSessionCompat().setSessionActivity(sessionActivity);
ImmutableList<ControllerInfo> connectedControllers =
sessionStub.getConnectedControllersManager().getConnectedControllers();
for (int i = 0; i < connectedControllers.size(); i++) {
ControllerInfo controllerInfo = connectedControllers.get(i);
if (controllerInfo.getControllerVersion() >= 3) {
dispatchRemoteControllerTaskWithoutReturn(
controllerInfo,
(controller, seq) -> controller.onSessionActivityChanged(seq, sessionActivity));
}
}
}
/**
* Gets the service binder from the MediaBrowserServiceCompat. Should be only called by the thread
* with a Looper.

View file

@ -55,6 +55,7 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCR
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING;
import android.app.PendingIntent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@ -1846,6 +1847,12 @@ import java.util.concurrent.ExecutionException;
iController.onSetCustomLayout(sequenceNumber, BundleableUtil.toBundleList(layout));
}
@Override
public void onSessionActivityChanged(int sequenceNumber, PendingIntent sessionActivity)
throws RemoteException {
iController.onSessionActivityChanged(sequenceNumber, sessionActivity);
}
@Override
public void onAvailableCommandsChangedFromSession(
int sequenceNumber, SessionCommands sessionCommands, Player.Commands playerCommands)

View file

@ -34,6 +34,7 @@ interface IRemoteMediaSession {
void setCustomLayout(String sessionId, in List<Bundle> layout);
void setSessionExtras(String sessionId, in Bundle extras);
void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras);
void setSessionActivity(String sessionId, in PendingIntent sessionActivity);
// Player Methods
void setPlayWhenReady(String sessionId, boolean playWhenReady, int reason);

View file

@ -25,7 +25,9 @@ import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
@ -51,6 +53,7 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.PollingCheck;
import androidx.media3.test.session.common.SurfaceActivity;
import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
@ -980,6 +983,32 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(TestUtils.equals(receivedSessionExtras.get(1), sessionExtras)).isTrue();
}
@Test
public void setSessionActivity_changedWhenReceivedWithSetter() throws Exception {
Intent intent = new Intent(context, SurfaceActivity.class);
PendingIntent sessionActivity =
PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
CountDownLatch latch = new CountDownLatch(1);
MediaControllerCompat.Callback callback =
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
latch.countDown();
}
};
controllerCompat.registerCallback(callback, handler);
assertThat(controllerCompat.getSessionActivity()).isNull();
session.setSessionActivity(sessionActivity);
// The legacy API has no change listener for the session activity. Changing the state to
// trigger a callback.
session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerCompat.getSessionActivity()).isEqualTo(sessionActivity);
}
@Test
public void broadcastCustomCommand_cnSessionEventCalled() throws Exception {
Bundle commandCallExtras = new Bundle();

View file

@ -34,7 +34,9 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.RemoteException;
@ -63,6 +65,7 @@ import androidx.media3.common.text.CueGroup;
import androidx.media3.session.RemoteMediaSession.RemoteMockPlayer;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.SurfaceActivity;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -2511,6 +2514,35 @@ public class MediaControllerListenerTest {
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
}
@Test
public void setSessionActivity_onSessionActivityChangedCalled() throws Exception {
Intent intent = new Intent(context, SurfaceActivity.class);
PendingIntent sessionActivity =
PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
CountDownLatch latch = new CountDownLatch(1);
List<PendingIntent> receivedSessionActivities = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) {
latch.countDown();
receivedSessionActivities.add(sessionActivity);
}
};
MediaController controller =
controllerTestRule.createController(
remoteSession.getToken(), /* connectionHints= */ null, listener);
assertThat(controller.getSessionActivity()).isNull();
remoteSession.setSessionActivity(sessionActivity);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(receivedSessionActivities).containsExactly(sessionActivity);
}
@Test
public void onVideoSizeChanged() throws Exception {
VideoSize testVideoSize =

View file

@ -492,6 +492,12 @@ public class MediaSessionProviderService extends Service {
});
}
@Override
public void setSessionActivity(String sessionId, PendingIntent sessionActivity)
throws RemoteException {
runOnHandler(() -> sessionMap.get(sessionId).setSessionActivity(sessionActivity));
}
////////////////////////////////////////////////////////////////////////////////
// MockPlayer methods
////////////////////////////////////////////////////////////////////////////////

View file

@ -59,6 +59,7 @@ import static androidx.media3.test.session.common.TestUtils.SERVICE_CONNECTION_T
import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@ -207,6 +208,10 @@ public class RemoteMediaSession {
binder.setSessionExtrasForController(sessionId, controllerKey, extras);
}
public void setSessionActivity(PendingIntent sessionActivity) throws RemoteException {
binder.setSessionActivity(sessionId, sessionActivity);
}
////////////////////////////////////////////////////////////////////////////////
// RemoteMockPlayer methods
////////////////////////////////////////////////////////////////////////////////

View file

@ -15,6 +15,7 @@
*/
package androidx.media3.session;
import android.app.PendingIntent;
import android.os.Bundle;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
@ -61,6 +62,11 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener {
delegate.onExtrasChanged(controller, extras);
}
@Override
public void onSessionActivityChanged(MediaController controller, PendingIntent sessionActivity) {
delegate.onSessionActivityChanged(controller, sessionActivity);
}
@Override
public void onAvailableSessionCommandsChanged(
MediaController controller, SessionCommands commands) {