Compare commits

...

6 Commits

Author SHA1 Message Date
jbibik
4045acd13b Fix Shuffle and Repeat references in tests
PiperOrigin-RevId: 728309204
2025-02-18 12:14:18 -08:00
shahddaghash
90844f11e8 Add test for usePlatformDiagnostics disabled
PiperOrigin-RevId: 728276994
2025-02-18 10:57:48 -08:00
tonihei
f1c62c1239 Make SimpleBasePlayer.State public
This ensures it's easier to handle these State updates in other
helper classes if needed.

Issue: androidx/media#2128

#cherrypick

PiperOrigin-RevId: 728264396
2025-02-18 10:28:05 -08:00
tonihei
4b991ad42f Decouple gradle publish task from lint and test
The publish task currently forces to run lint and test
even though there is no technical dependency. If a
process (e.g. releasing a new library) wants to verify
lint and test work, it should run these steps explicitly.

#cherrypick

PiperOrigin-RevId: 728234811
2025-02-18 09:11:29 -08:00
michaelkatz
75ac9cd191 Supply MediaPeriodId in ExoPlaybackExceptions thrown from Renderers
PiperOrigin-RevId: 728177353
2025-02-18 06:06:17 -08:00
bachinger
2b8700beaa Make setSessionActivity accept null
Issue: androidx/media#2109
PiperOrigin-RevId: 728160580
2025-02-18 05:06:06 -08:00
26 changed files with 193 additions and 56 deletions

View File

@ -3,6 +3,9 @@
### Unreleased changes ### Unreleased changes
* Common Library: * Common Library:
* Change `SimpleBasePlayer.State` access from protected to public to make
it easier to handle updates in other classes
([#2128](https://github.com/androidx/media/issues/2128)).
* ExoPlayer: * ExoPlayer:
* Transformer: * Transformer:
* Track Selection: * Track Selection:
@ -19,6 +22,8 @@
* Muxers: * Muxers:
* IMA extension: * IMA extension:
* Session: * Session:
* Make `MediaSession.setSessionActivity(PendingIntent)` accept null
([#2109](https://github.com/androidx/media/issues/2109)).
* UI: * UI:
* Downloads: * Downloads:
* OkHttp Extension: * OkHttp Extension:

View File

@ -96,7 +96,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public abstract class SimpleBasePlayer extends BasePlayer { public abstract class SimpleBasePlayer extends BasePlayer {
/** An immutable state description of the player. */ /** An immutable state description of the player. */
protected static final class State { public static final class State {
/** A builder for {@link State} objects. */ /** A builder for {@link State} objects. */
public static final class Builder { public static final class Builder {

View File

@ -84,6 +84,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private boolean streamIsFinal; private boolean streamIsFinal;
private boolean throwRendererExceptionIsExecuting; private boolean throwRendererExceptionIsExecuting;
private Timeline timeline; private Timeline timeline;
@Nullable private MediaSource.MediaPeriodId mediaPeriodId;
@GuardedBy("lock") @GuardedBy("lock")
@Nullable @Nullable
@ -144,6 +145,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
throws ExoPlaybackException { throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED); Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration; this.configuration = configuration;
this.mediaPeriodId = mediaPeriodId;
state = STATE_ENABLED; state = STATE_ENABLED;
onEnabled(joining, mayRenderStartOfStream); onEnabled(joining, mayRenderStartOfStream);
replaceStream(formats, stream, startPositionUs, offsetUs, mediaPeriodId); replaceStream(formats, stream, startPositionUs, offsetUs, mediaPeriodId);
@ -167,6 +169,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
throws ExoPlaybackException { throws ExoPlaybackException {
Assertions.checkState(!streamIsFinal); Assertions.checkState(!streamIsFinal);
this.stream = stream; this.stream = stream;
this.mediaPeriodId = mediaPeriodId;
if (readingPositionUs == C.TIME_END_OF_SOURCE) { if (readingPositionUs == C.TIME_END_OF_SOURCE) {
readingPositionUs = startPositionUs; readingPositionUs = startPositionUs;
} }
@ -242,6 +245,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
streamFormats = null; streamFormats = null;
streamIsFinal = false; streamIsFinal = false;
onDisabled(); onDisabled();
mediaPeriodId = null;
} }
@Override @Override
@ -485,6 +489,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
return timeline; return timeline;
} }
/**
* The {@link MediaSource.MediaPeriodId} of the {@link MediaPeriod} producing the {@code stream},
* or {@code null} if the renderer is disabled.
*/
@Nullable
protected final MediaSource.MediaPeriodId getMediaPeriodId() {
return mediaPeriodId;
}
/** /**
* Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for
* this renderer. * this renderer.
@ -530,7 +543,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
} }
return ExoPlaybackException.createForRenderer( return ExoPlaybackException.createForRenderer(
cause, getName(), getIndex(), format, formatSupport, isRecoverable, errorCode); cause,
getName(),
getIndex(),
format,
formatSupport,
mediaPeriodId,
isRecoverable,
errorCode);
} }
/** /**

View File

@ -131,6 +131,31 @@ public final class ExoPlaybackException extends PlaybackException {
return new ExoPlaybackException(TYPE_SOURCE, cause, errorCode); return new ExoPlaybackException(TYPE_SOURCE, cause, errorCode);
} }
/**
* @deprecated Use {@link #createForRenderer(Throwable, String, int, Format, int, MediaPeriodId,
* boolean, int)} instead.
*/
@Deprecated
@UnstableApi
public static ExoPlaybackException createForRenderer(
Throwable cause,
String rendererName,
int rendererIndex,
@Nullable Format rendererFormat,
@FormatSupport int rendererFormatSupport,
boolean isRecoverable,
@ErrorCode int errorCode) {
return createForRenderer(
cause,
rendererName,
rendererIndex,
rendererFormat,
rendererFormatSupport,
/* mediaPeriodId= */ null,
isRecoverable,
errorCode);
}
/** /**
* Creates an instance of type {@link #TYPE_RENDERER}. * Creates an instance of type {@link #TYPE_RENDERER}.
* *
@ -142,6 +167,8 @@ public final class ExoPlaybackException extends PlaybackException {
* or null if the renderer wasn't using a {@link Format}. * or null if the renderer wasn't using a {@link Format}.
* @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code
* rendererFormat}. Ignored if {@code rendererFormat} is null. * rendererFormat}. Ignored if {@code rendererFormat} is null.
* @param mediaPeriodId The {@link MediaPeriodId mediaPeriodId} of the media associated with this
* error, or null if undetermined.
* @param isRecoverable If the failure can be recovered by disabling and re-enabling the renderer. * @param isRecoverable If the failure can be recovered by disabling and re-enabling the renderer.
* @param errorCode See {@link #errorCode}. * @param errorCode See {@link #errorCode}.
* @return The created instance. * @return The created instance.
@ -153,9 +180,9 @@ public final class ExoPlaybackException extends PlaybackException {
int rendererIndex, int rendererIndex,
@Nullable Format rendererFormat, @Nullable Format rendererFormat,
@FormatSupport int rendererFormatSupport, @FormatSupport int rendererFormatSupport,
@Nullable MediaPeriodId mediaPeriodId,
boolean isRecoverable, boolean isRecoverable,
@ErrorCode int errorCode) { @ErrorCode int errorCode) {
return new ExoPlaybackException( return new ExoPlaybackException(
TYPE_RENDERER, TYPE_RENDERER,
cause, cause,
@ -165,6 +192,7 @@ public final class ExoPlaybackException extends PlaybackException {
rendererIndex, rendererIndex,
rendererFormat, rendererFormat,
rendererFormat == null ? C.FORMAT_HANDLED : rendererFormatSupport, rendererFormat == null ? C.FORMAT_HANDLED : rendererFormatSupport,
mediaPeriodId,
isRecoverable); isRecoverable);
} }
@ -208,6 +236,7 @@ public final class ExoPlaybackException extends PlaybackException {
/* rendererIndex= */ C.INDEX_UNSET, /* rendererIndex= */ C.INDEX_UNSET,
/* rendererFormat= */ null, /* rendererFormat= */ null,
/* rendererFormatSupport= */ C.FORMAT_HANDLED, /* rendererFormatSupport= */ C.FORMAT_HANDLED,
/* mediaPeriodId= */ null,
/* isRecoverable= */ false); /* isRecoverable= */ false);
} }
@ -221,6 +250,7 @@ public final class ExoPlaybackException extends PlaybackException {
/* rendererIndex= */ C.INDEX_UNSET, /* rendererIndex= */ C.INDEX_UNSET,
/* rendererFormat= */ null, /* rendererFormat= */ null,
/* rendererFormatSupport= */ C.FORMAT_HANDLED, /* rendererFormatSupport= */ C.FORMAT_HANDLED,
/* mediaPeriodId= */ null,
/* isRecoverable= */ false); /* isRecoverable= */ false);
} }
@ -233,6 +263,7 @@ public final class ExoPlaybackException extends PlaybackException {
int rendererIndex, int rendererIndex,
@Nullable Format rendererFormat, @Nullable Format rendererFormat,
@FormatSupport int rendererFormatSupport, @FormatSupport int rendererFormatSupport,
@Nullable MediaPeriodId mediaPeriodId,
boolean isRecoverable) { boolean isRecoverable) {
this( this(
deriveMessage( deriveMessage(
@ -249,7 +280,7 @@ public final class ExoPlaybackException extends PlaybackException {
rendererIndex, rendererIndex,
rendererFormat, rendererFormat,
rendererFormatSupport, rendererFormatSupport,
/* mediaPeriodId= */ null, mediaPeriodId,
/* timestampMs= */ SystemClock.elapsedRealtime(), /* timestampMs= */ SystemClock.elapsedRealtime(),
isRecoverable); isRecoverable);
} }

View File

@ -693,7 +693,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
if (e.type == ExoPlaybackException.TYPE_RENDERER) { if (e.type == ExoPlaybackException.TYPE_RENDERER) {
@Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod();
if (readingPeriod != null) { if (readingPeriod != null && e.mediaPeriodId == null) {
// We can assume that all renderer errors happen in the context of the reading period. See // We can assume that all renderer errors happen in the context of the reading period. See
// [internal: b/150584930#comment4] for exceptions that aren't covered by this assumption. // [internal: b/150584930#comment4] for exceptions that aren't covered by this assumption.
e = e =

View File

@ -312,6 +312,7 @@ public class MediaCodecAudioRendererTest {
/* rendererIndex= */ 0, /* rendererIndex= */ 0,
format, format,
C.FORMAT_HANDLED, C.FORMAT_HANDLED,
/* mediaPeriodId= */ null,
/* isRecoverable= */ false, /* isRecoverable= */ false,
PlaybackException.ERROR_CODE_UNSPECIFIED)); PlaybackException.ERROR_CODE_UNSPECIFIED));
} }

View File

@ -48,7 +48,7 @@ oneway interface IMediaController {
int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009; int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009;
void onRenderedFirstFrame(int seq) = 3010; void onRenderedFirstFrame(int seq) = 3010;
void onExtrasChanged(int seq, in Bundle extras) = 3011; void onExtrasChanged(int seq, in Bundle extras) = 3011;
void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013; void onSessionActivityChanged(int seq, in @nullable PendingIntent pendingIntent) = 3013;
void onError(int seq, in Bundle sessionError) = 3014; void onError(int seq, in Bundle sessionError) = 3014;
void onSetMediaButtonPreferences(int seq, in List<Bundle> commandButtonList) = 3015; void onSetMediaButtonPreferences(int seq, in List<Bundle> commandButtonList) = 3015;
// Next Id for MediaController: 3016 // Next Id for MediaController: 3016

View File

@ -493,7 +493,7 @@ public class MediaController implements Player {
*/ */
@UnstableApi @UnstableApi
default void onSessionActivityChanged( default void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) {} MediaController controller, @Nullable PendingIntent sessionActivity) {}
/** /**
* Called when an non-fatal error {@linkplain * Called when an non-fatal error {@linkplain

View File

@ -93,6 +93,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -3079,8 +3080,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}); });
} }
public void onSetSessionActivity(int seq, PendingIntent sessionActivity) { public void onSetSessionActivity(int seq, @Nullable PendingIntent sessionActivity) {
if (!isConnected()) { if (!isConnected() || Objects.equals(this.sessionActivity, sessionActivity)) {
return; return;
} }
this.sessionActivity = sessionActivity; this.sessionActivity = sessionActivity;

View File

@ -214,10 +214,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
@Override @Override
public void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity) public void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity)
throws RemoteException { throws RemoteException {
if (sessionActivity == null) {
Log.w(TAG, "Ignoring null session activity intent");
return;
}
dispatchControllerTaskOnHandler( dispatchControllerTaskOnHandler(
controller -> controller.onSetSessionActivity(seq, sessionActivity)); controller -> controller.onSetSessionActivity(seq, sessionActivity));
} }

View File

@ -789,13 +789,16 @@ public class MediaSession {
* Updates the session activity that was set when {@linkplain * Updates the session activity that was set when {@linkplain
* Builder#setSessionActivity(PendingIntent) building the session}. * Builder#setSessionActivity(PendingIntent) building the session}.
* *
* @param activityPendingIntent The pending intent to start the session activity. * <p>Note: When a controller is connected to the session that has a version smaller than 1.6.0,
* then setting the session activity to null has no effect on the controller side.
*
* @param activityPendingIntent The pending intent to start the session activity or null.
* @throws IllegalArgumentException if the {@link PendingIntent} passed into this method is * @throws IllegalArgumentException if the {@link PendingIntent} passed into this method is
* {@linkplain PendingIntent#getActivity(Context, int, Intent, int) not an activity}. * {@linkplain PendingIntent#getActivity(Context, int, Intent, int) not an activity}.
*/ */
@UnstableApi @UnstableApi
public final void setSessionActivity(PendingIntent activityPendingIntent) { public final void setSessionActivity(@Nullable PendingIntent activityPendingIntent) {
if (Util.SDK_INT >= 31) { if (Util.SDK_INT >= 31 && activityPendingIntent != null) {
checkArgument(Api31.isActivity(activityPendingIntent)); checkArgument(Api31.isActivity(activityPendingIntent));
} }
impl.setSessionActivity(activityPendingIntent); impl.setSessionActivity(activityPendingIntent);
@ -818,8 +821,8 @@ public class MediaSession {
*/ */
@UnstableApi @UnstableApi
public final void setSessionActivity( public final void setSessionActivity(
ControllerInfo controller, PendingIntent activityPendingIntent) { ControllerInfo controller, @Nullable PendingIntent activityPendingIntent) {
if (Util.SDK_INT >= 31) { if (Util.SDK_INT >= 31 && activityPendingIntent != null) {
checkArgument(Api31.isActivity(activityPendingIntent)); checkArgument(Api31.isActivity(activityPendingIntent));
} }
impl.setSessionActivity(controller, activityPendingIntent); impl.setSessionActivity(controller, activityPendingIntent);
@ -2088,7 +2091,7 @@ public class MediaSession {
default void setMediaButtonPreferences(int seq, List<CommandButton> mediaButtonPreferences) default void setMediaButtonPreferences(int seq, List<CommandButton> mediaButtonPreferences)
throws RemoteException {} throws RemoteException {}
default void onSessionActivityChanged(int seq, PendingIntent sessionActivity) default void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity)
throws RemoteException {} throws RemoteException {}
default void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {} default void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {}

View File

@ -95,7 +95,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ class MediaSessionImpl { /* package */ class MediaSessionImpl {
@ -136,7 +135,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private PlayerInfo playerInfo; private PlayerInfo playerInfo;
private PlayerWrapper playerWrapper; private PlayerWrapper playerWrapper;
private @MonotonicNonNull PendingIntent sessionActivity; @Nullable private PendingIntent sessionActivity;
@Nullable private PlayerListener playerListener; @Nullable private PlayerListener playerListener;
@Nullable private MediaSession.Listener mediaSessionListener; @Nullable private MediaSession.Listener mediaSessionListener;
@Nullable private ControllerInfo controllerForCurrentRequest; @Nullable private ControllerInfo controllerForCurrentRequest;
@ -850,7 +849,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@UnstableApi @UnstableApi
protected void setSessionActivity(PendingIntent sessionActivity) { protected void setSessionActivity(@Nullable PendingIntent sessionActivity) {
this.sessionActivity = sessionActivity; this.sessionActivity = sessionActivity;
ImmutableList<ControllerInfo> connectedControllers = ImmutableList<ControllerInfo> connectedControllers =
sessionStub.getConnectedControllersManager().getConnectedControllers(); sessionStub.getConnectedControllersManager().getConnectedControllers();
@ -860,7 +859,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@UnstableApi @UnstableApi
protected void setSessionActivity(ControllerInfo controller, PendingIntent sessionActivity) { protected void setSessionActivity(
ControllerInfo controller, @Nullable PendingIntent sessionActivity) {
if (controller.getControllerVersion() >= 3 if (controller.getControllerVersion() >= 3
&& sessionStub.getConnectedControllersManager().isConnected(controller)) { && sessionStub.getConnectedControllersManager().isConnected(controller)) {
dispatchRemoteControllerTaskWithoutReturn( dispatchRemoteControllerTaskWithoutReturn(

View File

@ -1137,7 +1137,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
@Override @Override
public void onSessionActivityChanged(int seq, PendingIntent sessionActivity) { public void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity) {
sessionCompat.setSessionActivity(sessionActivity); sessionCompat.setSessionActivity(sessionActivity);
} }

View File

@ -2098,9 +2098,10 @@ import java.util.concurrent.ExecutionException;
} }
} }
@SuppressWarnings("nullness:argument") // sessionActivity can be null.
@Override @Override
public void onSessionActivityChanged(int sequenceNumber, PendingIntent sessionActivity) public void onSessionActivityChanged(
throws RemoteException { int sequenceNumber, @Nullable PendingIntent sessionActivity) throws RemoteException {
iController.onSessionActivityChanged(sequenceNumber, sessionActivity); iController.onSessionActivityChanged(sequenceNumber, sessionActivity);
} }

View File

@ -550,7 +550,7 @@ public class MediaSessionCompat {
* *
* @param pi The intent to launch to show UI for this Session. * @param pi The intent to launch to show UI for this Session.
*/ */
public void setSessionActivity(PendingIntent pi) { public void setSessionActivity(@Nullable PendingIntent pi) {
mImpl.setSessionActivity(pi); mImpl.setSessionActivity(pi);
} }
@ -2219,7 +2219,7 @@ public class MediaSessionCompat {
void setMetadata(@Nullable MediaMetadataCompat metadata); void setMetadata(@Nullable MediaMetadataCompat metadata);
void setSessionActivity(PendingIntent pi); void setSessionActivity(@Nullable PendingIntent pi);
void setMediaButtonReceiver(@Nullable PendingIntent mbr); void setMediaButtonReceiver(@Nullable PendingIntent mbr);
@ -2700,7 +2700,7 @@ public class MediaSessionCompat {
} }
@Override @Override
public void setSessionActivity(PendingIntent pi) { public void setSessionActivity(@Nullable PendingIntent pi) {
synchronized (mLock) { synchronized (mLock) {
mSessionActivity = pi; mSessionActivity = pi;
} }
@ -4053,7 +4053,7 @@ public class MediaSessionCompat {
} }
@Override @Override
public void setSessionActivity(PendingIntent pi) { public void setSessionActivity(@Nullable PendingIntent pi) {
mSessionFwk.setSessionActivity(pi); mSessionFwk.setSessionActivity(pi);
} }

View File

@ -1217,6 +1217,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(playbackStateExtrasFromController.get()).string("key-0").isEqualTo("value-0"); assertThat(playbackStateExtrasFromController.get()).string("key-0").isEqualTo("value-0");
} }
@SuppressWarnings("deprecation") // Testing access through deprecated androidx.media library
@Test @Test
public void setSessionActivity_forAllControllers_changedWhenReceivedWithSetter() public void setSessionActivity_forAllControllers_changedWhenReceivedWithSetter()
throws Exception { throws Exception {
@ -1224,12 +1225,17 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
PendingIntent sessionActivity = PendingIntent sessionActivity =
PendingIntent.getActivity( PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
CountDownLatch latch = new CountDownLatch(1); CountDownLatch playingLatch = new CountDownLatch(1);
CountDownLatch bufferingLatch = new CountDownLatch(1);
MediaControllerCompat.Callback callback = MediaControllerCompat.Callback callback =
new MediaControllerCompat.Callback() { new MediaControllerCompat.Callback() {
@Override @Override
public void onPlaybackStateChanged(PlaybackStateCompat state) { public void onPlaybackStateChanged(PlaybackStateCompat state) {
latch.countDown(); if (state.getState() == PlaybackStateCompat.STATE_BUFFERING) {
bufferingLatch.countDown();
} else if (state.getState() == PlaybackStateCompat.STATE_PLAYING) {
playingLatch.countDown();
}
} }
}; };
controllerCompat.registerCallback(callback, handler); controllerCompat.registerCallback(callback, handler);
@ -1238,12 +1244,25 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
session.setSessionActivity(/* controllerKey= */ null, sessionActivity); session.setSessionActivity(/* controllerKey= */ null, sessionActivity);
// The legacy API has no change listener for the session activity. Changing the state to // The legacy API has no change listener for the session activity. Changing the state to
// trigger a callback. // trigger a callback.
session
.getMockPlayer()
.notifyPlayWhenReadyChanged(
true,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY); session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(playingLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerCompat.getSessionActivity()).isEqualTo(sessionActivity); assertThat(controllerCompat.getSessionActivity()).isEqualTo(sessionActivity);
session.setSessionActivity(/* controllerKey= */ null, null);
session.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_BUFFERING);
assertThat(bufferingLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerCompat.getSessionActivity()).isNull();
} }
@SuppressWarnings("deprecation") // Testing access through deprecated androidx.media library
@Test @Test
public void setSessionActivity_setToNotificationController_changedWhenReceivedWithSetter() public void setSessionActivity_setToNotificationController_changedWhenReceivedWithSetter()
throws Exception { throws Exception {
@ -1251,24 +1270,41 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
PendingIntent sessionActivity = PendingIntent sessionActivity =
PendingIntent.getActivity( PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
CountDownLatch latch = new CountDownLatch(1); CountDownLatch playingLatch = new CountDownLatch(1);
CountDownLatch bufferingLatch = new CountDownLatch(1);
MediaControllerCompat.Callback callback = MediaControllerCompat.Callback callback =
new MediaControllerCompat.Callback() { new MediaControllerCompat.Callback() {
@Override @Override
public void onPlaybackStateChanged(PlaybackStateCompat state) { public void onPlaybackStateChanged(PlaybackStateCompat state) {
latch.countDown(); if (state.getState() == PlaybackStateCompat.STATE_BUFFERING) {
bufferingLatch.countDown();
} else if (state.getState() == PlaybackStateCompat.STATE_PLAYING) {
playingLatch.countDown();
}
} }
}; };
controllerCompat.registerCallback(callback, handler); controllerCompat.registerCallback(callback, handler);
assertThat(controllerCompat.getSessionActivity()).isNull(); assertThat(controllerCompat.getSessionActivity()).isNull();
session.setSessionActivity(NOTIFICATION_CONTROLLER_KEY, sessionActivity); session.setSessionActivity(NOTIFICATION_CONTROLLER_KEY, sessionActivity);
session
.getMockPlayer()
.notifyPlayWhenReadyChanged(
true,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
// The legacy API has no change listener for the session activity. Changing the state to // The legacy API has no change listener for the session activity. Changing the state to
// trigger a callback. // trigger a callback.
session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY); session.getMockPlayer().notifyPlaybackStateChanged(STATE_READY);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(playingLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerCompat.getSessionActivity()).isEqualTo(sessionActivity); assertThat(controllerCompat.getSessionActivity()).isEqualTo(sessionActivity);
session.setSessionActivity(NOTIFICATION_CONTROLLER_KEY, null);
session.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_BUFFERING);
assertThat(bufferingLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerCompat.getSessionActivity()).isNull();
} }
@Test @Test

View File

@ -2566,14 +2566,19 @@ public class MediaControllerListenerTest {
PendingIntent.getActivity( PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
CountDownLatch nullLatch = new CountDownLatch(1);
List<PendingIntent> receivedSessionActivities = new ArrayList<>(); List<PendingIntent> receivedSessionActivities = new ArrayList<>();
MediaController.Listener listener = MediaController.Listener listener =
new MediaController.Listener() { new MediaController.Listener() {
@Override @Override
public void onSessionActivityChanged( public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) { MediaController controller, @Nullable PendingIntent sessionActivity) {
receivedSessionActivities.add(sessionActivity); if (sessionActivity == null) {
latch.countDown(); nullLatch.countDown();
} else {
receivedSessionActivities.add(sessionActivity);
latch.countDown();
}
} }
}; };
MediaController controller = MediaController controller =
@ -2586,6 +2591,13 @@ public class MediaControllerListenerTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller.getSessionActivity()).isEqualTo(sessionActivity); assertThat(controller.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(receivedSessionActivities).containsExactly(sessionActivity); assertThat(receivedSessionActivities).containsExactly(sessionActivity);
remoteSession.setSessionActivity(/* controllerKey= */ null, sessionActivity);
remoteSession.setSessionActivity(/* controllerKey= */ null, null);
assertThat(nullLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller.getSessionActivity()).isNull();
assertThat(receivedSessionActivities).containsExactly(sessionActivity);
} }
@Test @Test
@ -2601,7 +2613,7 @@ public class MediaControllerListenerTest {
new MediaController.Listener() { new MediaController.Listener() {
@Override @Override
public void onSessionActivityChanged( public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) { MediaController controller, @Nullable PendingIntent sessionActivity) {
receivedSessionActivities1.add(sessionActivity); receivedSessionActivities1.add(sessionActivity);
latch1.countDown(); latch1.countDown();
} }
@ -2610,15 +2622,17 @@ public class MediaControllerListenerTest {
connectionHints1.putString(KEY_CONTROLLER, "ctrl-1"); connectionHints1.putString(KEY_CONTROLLER, "ctrl-1");
MediaController controller1 = MediaController controller1 =
controllerTestRule.createController(remoteSession.getToken(), connectionHints1, listener1); controllerTestRule.createController(remoteSession.getToken(), connectionHints1, listener1);
List<PendingIntent> receivedSessionActivities2 = new ArrayList<>(); AtomicInteger controller2CallbackCount = new AtomicInteger();
CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1);
MediaController.Listener listener2 = MediaController.Listener listener2 =
new MediaController.Listener() { new MediaController.Listener() {
@Override @Override
public void onSessionActivityChanged( public void onSessionActivityChanged(
MediaController controller, PendingIntent sessionActivity) { MediaController controller, @Nullable PendingIntent sessionActivity) {
receivedSessionActivities2.add(sessionActivity); controller2CallbackCount.incrementAndGet();
latch2.countDown(); if (sessionActivity == null) {
latch2.countDown();
}
} }
}; };
Bundle connectionHints2 = new Bundle(); Bundle connectionHints2 = new Bundle();
@ -2634,14 +2648,16 @@ public class MediaControllerListenerTest {
assertThat(controller1.getSessionActivity()).isEqualTo(sessionActivity); assertThat(controller1.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(controller2.getSessionActivity()).isNull(); assertThat(controller2.getSessionActivity()).isNull();
assertThat(receivedSessionActivities1).containsExactly(sessionActivity); assertThat(receivedSessionActivities1).containsExactly(sessionActivity);
assertThat(receivedSessionActivities2).isEmpty(); assertThat(controller2CallbackCount.get()).isEqualTo(0);
remoteSession.setSessionActivity(/* controllerKey= */ "ctrl-2", sessionActivity); remoteSession.setSessionActivity(/* controllerKey= */ "ctrl-2", sessionActivity);
remoteSession.setSessionActivity(/* controllerKey= */ "ctrl-2", null);
assertThat(latch2.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch2.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controller2.getSessionActivity()).isEqualTo(sessionActivity); assertThat(controller1.getSessionActivity()).isEqualTo(sessionActivity);
assertThat(controller2.getSessionActivity()).isNull();
assertThat(receivedSessionActivities1).containsExactly(sessionActivity); assertThat(receivedSessionActivities1).containsExactly(sessionActivity);
assertThat(receivedSessionActivities2).containsExactly(sessionActivity); assertThat(controller2CallbackCount.get()).isEqualTo(2);
} }
@Test @Test

View File

@ -703,7 +703,7 @@ public class MediaSessionProviderService extends Service {
@Override @Override
public void setSessionActivity( public void setSessionActivity(
String sessionId, @Nullable String controllerKey, PendingIntent sessionActivity) String sessionId, @Nullable String controllerKey, @Nullable PendingIntent sessionActivity)
throws RemoteException { throws RemoteException {
MediaSession mediaSession = sessionMap.get(sessionId); MediaSession mediaSession = sessionMap.get(sessionId);
if (mediaSession == null) { if (mediaSession == null) {

View File

@ -218,7 +218,7 @@ public class RemoteMediaSession {
binder.setSessionExtrasForController(sessionId, controllerKey, extras); binder.setSessionExtrasForController(sessionId, controllerKey, extras);
} }
public void setSessionActivity(String controllerKey, PendingIntent sessionActivity) public void setSessionActivity(String controllerKey, @Nullable PendingIntent sessionActivity)
throws RemoteException { throws RemoteException {
binder.setSessionActivity(sessionId, controllerKey, sessionActivity); binder.setSessionActivity(sessionId, controllerKey, sessionActivity);
} }

View File

@ -156,7 +156,7 @@ public class RemoteMediaSessionCompat {
binder.setShuffleMode(sessionTag, shuffleMode); binder.setShuffleMode(sessionTag, shuffleMode);
} }
public void setSessionActivity(PendingIntent intent) throws RemoteException { public void setSessionActivity(@Nullable PendingIntent intent) throws RemoteException {
binder.setSessionActivity(sessionTag, intent); binder.setSessionActivity(sessionTag, intent);
} }

View File

@ -79,7 +79,8 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener {
} }
@Override @Override
public void onSessionActivityChanged(MediaController controller, PendingIntent sessionActivity) { public void onSessionActivityChanged(
MediaController controller, @Nullable PendingIntent sessionActivity) {
delegate.onSessionActivityChanged(controller, sessionActivity); delegate.onSessionActivityChanged(controller, sessionActivity);
} }

View File

@ -120,6 +120,7 @@ public class FakeRenderer extends BaseRenderer {
getIndex(), getIndex(),
format, format,
C.FORMAT_UNSUPPORTED_TYPE, C.FORMAT_UNSUPPORTED_TYPE,
getMediaPeriodId(),
/* isRecoverable= */ false, /* isRecoverable= */ false,
PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
} }

View File

@ -19,6 +19,7 @@ import static androidx.media3.common.util.Util.usToMs;
import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET; import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported; import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
@ -44,6 +45,7 @@ import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.junit.AssumptionViolatedException;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -69,6 +71,30 @@ public class EditingMetricsCollectorTest {
testId = testName.getMethodName(); testId = testName.getMethodName();
} }
@Test
public void export_usePlatformDiagnosticsDisabled_doesNotCollectMetrics() throws Exception {
if (Util.SDK_INT < 35) {
String reason = "Metrics collection is unsupported below API 35.";
recordTestSkipped(context, testId, reason);
throw new AssumptionViolatedException(reason);
}
AtomicReference<EditingEndedEvent> editingEndedEventAtomicReference = new AtomicReference<>();
Transformer transformer =
new Transformer.Builder(context)
.setUsePlatformDiagnostics(false)
.setMetricsReporterFactory(
new TestMetricsReporterFactory(context, editingEndedEventAtomicReference::set))
.build();
EditedMediaItem audioVideoItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP4_ASSET.uri)).build();
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, audioVideoItem);
assertThat(editingEndedEventAtomicReference.get()).isNull();
}
@Test @Test
public void exportSuccess_populatesEditingEndedEvent() throws Exception { public void exportSuccess_populatesEditingEndedEvent() throws Exception {
assumeTrue("Reporting metrics requires API 35", Util.SDK_INT >= 35); assumeTrue("Reporting metrics requires API 35", Util.SDK_INT >= 35);

View File

@ -48,7 +48,7 @@ class RepeatButtonStateTest {
} }
@Test @Test
fun buttonClicked_withLimitedNumberOfModes_playerShuffleModeChangedToNextInSequence() { fun buttonClicked_withLimitedNumberOfModes_playerRepeatModeChangedToNextInSequence() {
val player = TestPlayer() val player = TestPlayer()
val state = RepeatButtonState(player, listOf(Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE)) val state = RepeatButtonState(player, listOf(Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE))
assertThat(state.repeatModeState).isEqualTo(Player.REPEAT_MODE_OFF) assertThat(state.repeatModeState).isEqualTo(Player.REPEAT_MODE_OFF)

View File

@ -60,9 +60,9 @@ class ShuffleButtonStateTest {
@Test @Test
fun playerSetShuffleModeAndOnClick_inTheSameHandlerMessage_uiStateSynchronises() { fun playerSetShuffleModeAndOnClick_inTheSameHandlerMessage_uiStateSynchronises() {
// The UDF model of Compose relies on holding the Player as the single source of truth with // The UDF model of Compose relies on holding the Player as the single source of truth with
// RepeatButtonState changing its state in sync with the relevant Player events. This means that // ShuffleButtonState changing its state in sync with the relevant Player events. This means
// we should never find ourselves in a situation where a button's icon (here: determined by // that we should never find ourselves in a situation where a button's icon (here: determined by
// RepeatButtonState.repeatModeState) is out of sync with the Player's repeat mode. It can cause // ShuffleButtonState.shuffleOn) is out of sync with the Player's shuffle mode. It can cause
// confusion for a human user whose intent to toggle the mode will not be fulfilled. The // confusion for a human user whose intent to toggle the mode will not be fulfilled. The
// following test tries to simulate this scenario by squeezing the 2 actions together (setter + // following test tries to simulate this scenario by squeezing the 2 actions together (setter +
// onClick) into a single Looper iteration. This is a practically unlikely scenario for a human // onClick) into a single Looper iteration. This is a practically unlikely scenario for a human

View File

@ -62,4 +62,3 @@ afterEvaluate {
} }
} }
} }
tasks.withType(PublishToMavenRepository) { it.dependsOn lint, test }