Allow session activity to be set per controller

#cherrypick

PiperOrigin-RevId: 644693533
This commit is contained in:
bachinger 2024-06-19 03:32:52 -07:00 committed by Copybara-Service
parent 30b9c976ea
commit 856d394c28
14 changed files with 256 additions and 27 deletions

View File

@ -17,6 +17,12 @@
* Muxers:
* IMA extension:
* Session:
* Allow the session activity to be set per controller to override the
global session activity. The session activity can be defined for a
controller at connection time by creating a `ConnectionResult` with
`AcceptedResultBuilder.setSessionActivivty(PendingIntent)`. Once
connected, the session activity can be updated with
`MediaSession.setSessionActivity(ControllerInfo, PendingIntent)`.
* UI:
* Downloads:
* OkHttp Extension:

View File

@ -759,6 +759,8 @@ public class MediaSession {
* Builder#setSessionActivity(PendingIntent) building the session}.
*
* @param activityPendingIntent The pending intent to start the session activity.
* @throws IllegalArgumentException if the {@link PendingIntent} passed into this method is
* {@linkplain PendingIntent#getActivity(Context, int, Intent, int) not an activity}.
*/
@UnstableApi
public final void setSessionActivity(PendingIntent activityPendingIntent) {
@ -768,6 +770,30 @@ public class MediaSession {
impl.setSessionActivity(activityPendingIntent);
}
/**
* Sends the session activity to the connected controller.
*
* <p>This call immediately returns and doesn't wait for a result from the controller.
*
* <p>Interoperability: This call has no effect when called for a {@linkplain
* ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}. To set the session activity of the
* platform session use {@linkplain #getMediaNotificationControllerInfo() the media notification
* controller} as the target controller.
*
* @param controller The controller to send the session activity to.
* @param activityPendingIntent The pending intent to start the session activity.
* @throws IllegalArgumentException if the {@link PendingIntent} passed into this method is
* {@linkplain PendingIntent#getActivity(Context, int, Intent, int) not an activity}.
*/
@UnstableApi
public final void setSessionActivity(
ControllerInfo controller, PendingIntent activityPendingIntent) {
if (Util.SDK_INT >= 31) {
checkArgument(Api31.isActivity(activityPendingIntent));
}
impl.setSessionActivity(controller, activityPendingIntent);
}
/**
* Sets the underlying {@link Player} for this session to dispatch incoming events to.
*
@ -1025,7 +1051,7 @@ public class MediaSession {
/**
* Broadcasts a custom command to all connected controllers.
*
* <p>This is a synchronous call and doesn't wait for results from the controllers.
* <p>This call immediately returns and doesn't wait for a result from the controller.
*
* <p>A command is not accepted if it is not a custom command.
*
@ -1061,7 +1087,7 @@ public class MediaSession {
* <p>The initial extras can be set {@linkplain Builder#setSessionExtras(Bundle) when building the
* session}.
*
* <p>This is a synchronous call and doesn't wait for results from the controllers.
* <p>This call immediately returns and doesn't wait for a result from the controller.
*
* @param sessionExtras The session extras.
*/
@ -1078,7 +1104,7 @@ public class MediaSession {
* ConnectionResult.AcceptedResultBuilder#setSessionExtras(Bundle) building the connection
* result}.
*
* <p>This is a synchronous call and doesn't wait for results from the controller.
* <p>This call immediately returns and doesn't wait for a result from the controller.
*
* <p>Interoperability: This call has no effect when called for a {@linkplain
* ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}.
@ -1713,6 +1739,7 @@ public class MediaSession {
private Player.Commands availablePlayerCommands = DEFAULT_PLAYER_COMMANDS;
@Nullable private ImmutableList<CommandButton> customLayout;
@Nullable private Bundle sessionExtras;
@Nullable private PendingIntent sessionActivity;
/**
* Creates an instance.
@ -1788,6 +1815,18 @@ public class MediaSession {
return this;
}
/**
* Sets the session activity, overriding the {@linkplain MediaSession#getSessionActivity()
* session activity of the session}.
*
* <p>The default is null to indicate that the session activity of the session should be used.
*/
@CanIgnoreReturnValue
public AcceptedResultBuilder setSessionActivity(@Nullable PendingIntent sessionActivity) {
this.sessionActivity = sessionActivity;
return this;
}
/** Returns a new {@link ConnectionResult} instance for accepting a connection. */
public ConnectionResult build() {
return new ConnectionResult(
@ -1795,7 +1834,8 @@ public class MediaSession {
availableSessionCommands,
availablePlayerCommands,
customLayout,
sessionExtras);
sessionExtras,
sessionActivity);
}
}
@ -1826,18 +1866,23 @@ public class MediaSession {
/** The session extras. */
@UnstableApi @Nullable public final Bundle sessionExtras;
/** The session activity. */
@UnstableApi @Nullable public final PendingIntent sessionActivity;
/** Creates a new instance with the given available session and player commands. */
private ConnectionResult(
boolean accepted,
SessionCommands availableSessionCommands,
Player.Commands availablePlayerCommands,
@Nullable ImmutableList<CommandButton> customLayout,
@Nullable Bundle sessionExtras) {
@Nullable Bundle sessionExtras,
@Nullable PendingIntent sessionActivity) {
isAccepted = accepted;
this.availableSessionCommands = availableSessionCommands;
this.availablePlayerCommands = availablePlayerCommands;
this.customLayout = customLayout;
this.sessionExtras = sessionExtras;
this.sessionActivity = sessionActivity;
}
/**
@ -1857,7 +1902,8 @@ public class MediaSession {
availableSessionCommands,
availablePlayerCommands,
/* customLayout= */ null,
/* sessionExtras= */ null);
/* sessionExtras= */ null,
/* sessionActivity= */ null);
}
/** Creates a {@link ConnectionResult} to reject a connection. */
@ -1867,7 +1913,8 @@ public class MediaSession {
SessionCommands.EMPTY,
Player.Commands.EMPTY,
/* customLayout= */ ImmutableList.of(),
/* sessionExtras= */ Bundle.EMPTY);
/* sessionExtras= */ Bundle.EMPTY,
/* sessionActivity= */ null);
}
}

View File

@ -783,19 +783,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@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));
setSessionActivity(connectedControllers.get(i), sessionActivity);
}
}
@UnstableApi
protected void setSessionActivity(ControllerInfo controller, PendingIntent sessionActivity) {
if (controller.getControllerVersion() >= 3
&& sessionStub.getConnectedControllersManager().isConnected(controller)) {
dispatchRemoteControllerTaskWithoutReturn(
controller, (callback, seq) -> callback.onSessionActivityChanged(seq, sessionActivity));
if (isMediaNotificationController(controller)) {
dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onSessionActivityChanged(seq, sessionActivity));
}
}
}

View File

@ -1117,6 +1117,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
sessionCompat.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
}
@Override
public void onSessionActivityChanged(int seq, PendingIntent sessionActivity) {
sessionCompat.setSessionActivity(sessionActivity);
}
@Override
public void onError(int seq, SessionError sessionError) {
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();

View File

@ -526,7 +526,9 @@ import java.util.concurrent.ExecutionException;
MediaLibraryInfo.VERSION_INT,
MediaSessionStub.VERSION_INT,
MediaSessionStub.this,
sessionImpl.getSessionActivity(),
connectionResult.sessionActivity != null
? connectionResult.sessionActivity
: sessionImpl.getSessionActivity(),
connectionResult.customLayout != null
? connectionResult.customLayout
: sessionImpl.getCustomLayout(),

View File

@ -30,6 +30,9 @@ interface IRemoteMediaController {
// MediaController Methods
Bundle getConnectedSessionToken(String controllerId);
Bundle getSessionExtras(String controllerId);
Bundle getCustomLayout(String controllerId);
Bundle getAvailableCommands(String controllerId);
PendingIntent getSessionActivity(String controllerId);
void play(String controllerId);
void pause(String controllerId);
void setPlayWhenReady(String controllerId, boolean playWhenReady);
@ -101,8 +104,6 @@ interface IRemoteMediaController {
int page,
int pageSize,
in Bundle libraryParams);
Bundle getCustomLayout(String controllerId);
Bundle getAvailableCommands(String controllerId);
Bundle getItem(String controllerId, String mediaId);
Bundle search(String controllerId, String query, in Bundle libraryParams);
Bundle getSearchResult(

View File

@ -35,7 +35,7 @@ interface IRemoteMediaSession {
void setSessionExtras(String sessionId, in Bundle extras);
void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras);
void sendError(String sessionId, String controllerKey, in Bundle SessionError);
void setSessionActivity(String sessionId, in PendingIntent sessionActivity);
void setSessionActivity(String sessionId, String controllerKey, in PendingIntent sessionActivity);
// Player Methods
void setPlayWhenReady(String sessionId, boolean playWhenReady, int reason);

View File

@ -1227,7 +1227,8 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
}
@Test
public void setSessionActivity_changedWhenReceivedWithSetter() throws Exception {
public void setSessionActivity_forAllControllers_changedWhenReceivedWithSetter()
throws Exception {
Intent intent = new Intent(context, SurfaceActivity.class);
PendingIntent sessionActivity =
PendingIntent.getActivity(
@ -1243,7 +1244,34 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
controllerCompat.registerCallback(callback, handler);
assertThat(controllerCompat.getSessionActivity()).isNull();
session.setSessionActivity(sessionActivity);
session.setSessionActivity(/* controllerKey= */ null, 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 setSessionActivity_setToNotificationController_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(NOTIFICATION_CONTROLLER_KEY, sessionActivity);
// The legacy API has no change listener for the session activity. Changing the state to
// trigger a callback.
session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY);

View File

@ -2535,13 +2535,69 @@ public class MediaControllerListenerTest {
remoteSession.getToken(), /* connectionHints= */ null, listener);
assertThat(controller.getSessionActivity()).isNull();
remoteSession.setSessionActivity(sessionActivity);
remoteSession.setSessionActivity(/* controllerKey= */ null, sessionActivity);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(receivedSessionActivities).containsExactly(sessionActivity);
}
@Test
public void setSessionActivity_forSpecificController_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 latch1 = new CountDownLatch(1);
List<PendingIntent> receivedSessionActivities1 = new ArrayList<>();
MediaController.Listener listener1 =
new MediaController.Listener() {
@Override
public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) {
receivedSessionActivities1.add(sessionActivity);
latch1.countDown();
}
};
Bundle connectionHints1 = new Bundle();
connectionHints1.putString(KEY_CONTROLLER, "ctrl-1");
MediaController controller1 =
controllerTestRule.createController(remoteSession.getToken(), connectionHints1, listener1);
List<PendingIntent> receivedSessionActivities2 = new ArrayList<>();
CountDownLatch latch2 = new CountDownLatch(1);
MediaController.Listener listener2 =
new MediaController.Listener() {
@Override
public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) {
receivedSessionActivities2.add(sessionActivity);
latch2.countDown();
}
};
Bundle connectionHints2 = new Bundle();
connectionHints2.putString(KEY_CONTROLLER, "ctrl-2");
MediaController controller2 =
controllerTestRule.createController(remoteSession.getToken(), connectionHints2, listener2);
assertThat(controller1.getSessionActivity()).isNull();
assertThat(controller2.getSessionActivity()).isNull();
remoteSession.setSessionActivity(/* controllerKey= */ "ctrl-1", sessionActivity);
assertThat(latch1.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller1.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(controller2.getSessionActivity()).isNull();
assertThat(receivedSessionActivities1).containsExactly(sessionActivity);
assertThat(receivedSessionActivities2).isEmpty();
remoteSession.setSessionActivity(/* controllerKey= */ "ctrl-2", sessionActivity);
assertThat(latch2.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller2.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(receivedSessionActivities1).containsExactly(sessionActivity);
assertThat(receivedSessionActivities2).containsExactly(sessionActivity);
}
@Test
public void onVideoSizeChanged() throws Exception {
VideoSize defaultVideoSize = MediaTestUtils.createDefaultVideoSize();

View File

@ -29,7 +29,9 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.fail;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.Nullable;
@ -45,6 +47,7 @@ import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuild
import androidx.media3.session.MediaSession.ControllerInfo;
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.TestHandler;
import androidx.media3.test.session.common.TestUtils;
import androidx.media3.test.utils.TestExoPlayerBuilder;
@ -293,6 +296,49 @@ public class MediaSessionCallbackTest {
assertThat(remoteController.getSessionExtras().getString("origin")).isEqualTo("controller");
}
@Test
public void onConnect_connectionResultSessionActivitySet_usesControllerSpecificSessionActivity()
throws Exception {
Intent intent = new Intent(context, SurfaceActivity.class);
PendingIntent controllerSpecificSessionActivity =
PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
AcceptedResultBuilder connectionResult =
new AcceptedResultBuilder(session)
.setAvailablePlayerCommands(Player.Commands.EMPTY);
if (controller.getConnectionHints().getBoolean("want_session_activity", false)) {
connectionResult.setSessionActivity(controllerSpecificSessionActivity);
}
return connectionResult.build();
}
};
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player)
.setCallback(callback)
.setId(
"onConnect_connectionResultSessionActivitySet_usesControllerSpecificSessionActivity")
.build());
Bundle connectionHints = new Bundle();
connectionHints.putBoolean("want_session_activity", true);
RemoteMediaController remoteControllerWithSpecificSessionActivity =
remoteControllerTestRule.createRemoteController(
session.getToken(), /* waitForConnection= */ true, connectionHints);
RemoteMediaController remoteControllerWithoutSpecificSessionActivity =
remoteControllerTestRule.createRemoteController(
session.getToken(), /* waitForConnection= */ true, /* connectionHints= */ Bundle.EMPTY);
assertThat(remoteControllerWithSpecificSessionActivity.getSessionActivity())
.isEqualTo(controllerSpecificSessionActivity);
assertThat(remoteControllerWithoutSpecificSessionActivity.getSessionActivity()).isNull();
}
@Test
public void onConnect_connectionResultDefault_emptySessionExtras() throws Exception {
MediaSession session =

View File

@ -21,6 +21,7 @@ import static androidx.media3.test.session.common.CommonConstants.KEY_COMMAND_BU
import static androidx.media3.test.session.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@ -854,6 +855,12 @@ public class MediaControllerProviderService extends Service {
return runOnHandler(controller::getAvailableCommands).toBundle();
}
@Override
public PendingIntent getSessionActivity(String controllerId) throws RemoteException {
MediaController controller = mediaControllerMap.get(controllerId);
return runOnHandler(controller::getSessionActivity);
}
@Override
public Bundle getItem(String controllerId, String mediaId) throws RemoteException {
MediaBrowser browser = (MediaBrowser) mediaControllerMap.get(controllerId);

View File

@ -111,6 +111,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
/**
@ -607,9 +608,29 @@ public class MediaSessionProviderService extends Service {
}
@Override
public void setSessionActivity(String sessionId, PendingIntent sessionActivity)
public void setSessionActivity(
String sessionId, @Nullable String controllerKey, PendingIntent sessionActivity)
throws RemoteException {
runOnHandler(() -> sessionMap.get(sessionId).setSessionActivity(sessionActivity));
MediaSession mediaSession = sessionMap.get(sessionId);
if (mediaSession == null) {
return;
}
if (controllerKey == null) {
// Set to all controllers by using the global session method.
runOnHandler(() -> mediaSession.setSessionActivity(sessionActivity));
return;
}
List<ControllerInfo> connectedControllers = mediaSession.getConnectedControllers();
for (int i = 0; i < connectedControllers.size(); i++) {
ControllerInfo controllerInfo = connectedControllers.get(i);
@Nullable
String connectedControllerKey =
controllerInfo.getConnectionHints().getString(KEY_CONTROLLER);
if (Objects.equals(controllerKey, connectedControllerKey)) {
// Set to controller for that the test case has given the provided controllerKey.
runOnHandler(() -> mediaSession.setSessionActivity(controllerInfo, sessionActivity));
}
}
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -22,6 +22,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;
@ -393,6 +394,10 @@ public class RemoteMediaController {
return Player.Commands.fromBundle(commandsBundle);
}
public PendingIntent getSessionActivity() throws RemoteException {
return binder.getSessionActivity(controllerId);
}
////////////////////////////////////////////////////////////////////////////////
// Non-public methods
////////////////////////////////////////////////////////////////////////////////

View File

@ -209,8 +209,9 @@ public class RemoteMediaSession {
binder.setSessionExtrasForController(sessionId, controllerKey, extras);
}
public void setSessionActivity(PendingIntent sessionActivity) throws RemoteException {
binder.setSessionActivity(sessionId, sessionActivity);
public void setSessionActivity(String controllerKey, PendingIntent sessionActivity)
throws RemoteException {
binder.setSessionActivity(sessionId, controllerKey, sessionActivity);
}
public void sendError(@Nullable String controllerKey, SessionError sessionError)