Skip to content

Commit

Permalink
Update error state of legacy playback state if authentication fails
Browse files Browse the repository at this point in the history
This change adds the ability to update the error code of the PlaybackStateCompat in
cases we need this for backwards compatibility. It is applied in the least
intrusive way because normally, return values of a service method should not change
the state of the `PlaybackStateCompat`, just because it has nothing to do with the
playback state but rather with the state of the `MediaLibrarySession`.

For this reason only the error code `RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED`
is taken into account while all other error codes are not mapped to the
`PlaybackStateCompat'.

PiperOrigin-RevId: 438038852
  • Loading branch information
marcbaechinger authored and icbaker committed Apr 6, 2022
1 parent e699765 commit 3ac7e0e
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,24 @@ public static LibraryResult<ImmutableList<MediaItem>> ofItemList(
* @param errorCode The error code.
*/
public static <V> LibraryResult<V> ofError(@Code int errorCode) {
return ofError(errorCode, /* params= */ null);
}

/**
* Creates an instance with an unsuccessful {@link Code result code} and {@link LibraryParams} to
* describe the error.
*
* <p>{@code errorCode} must not be {@link #RESULT_SUCCESS}.
*
* @param errorCode The error code.
* @param params The optional parameters to describe the error.
*/
public static <V> LibraryResult<V> ofError(@Code int errorCode, @Nullable LibraryParams params) {
checkArgument(errorCode != RESULT_SUCCESS);
return new LibraryResult<>(
errorCode,
/* resultCode= */ errorCode,
SystemClock.elapsedRealtime(),
/* params= */ null,
/* params= */ params,
/* value= */ null,
VALUE_TYPE_ERROR);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ public final class MediaConstants {
*/
public static final String MEDIA_URI_QUERY_URI = "uri";

/**
* The extras key for the localized error resolution string.
*
* <p>See {@link
* androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL}.
*/
public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT =
"android.media.extras.ERROR_RESOLUTION_ACTION_LABEL";
/**
* The extras key for the error resolution intent.
*
* <p>See {@link
* androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT}.
*/
public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT =
"android.media.extras.ERROR_RESOLUTION_ACTION_INTENT";

/** The legacy status code for successful execution. */
public static final int STATUS_CODE_SUCCESS_COMPAT = -1;

/**
* The legacy error code for expired authentication.
*
* <p>See {@code PlaybackStateCompat#ERROR_CODE_AUTHENTICATION_EXPIRED}.
*/
public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3;

/* package */ static final String SESSION_COMMAND_ON_EXTRAS_CHANGED =
"androidx.media3.session.SESSION_COMMAND_ON_EXTRAS_CHANGED";
/* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;

import android.app.PendingIntent;
import android.content.Context;
Expand Down Expand Up @@ -118,19 +121,17 @@ public void notifySearchResultChanged(

public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler(
ControllerInfo browser, @Nullable LibraryParams params) {
// onGetLibraryRoot is defined to return a non-null result but it's implemented by applications,
// so we explicitly null-check the result to fail early if an app accidentally returns null.
return checkNotNull(
callback.onGetLibraryRoot(instance, browser, params),
"onGetLibraryRoot must return non-null future");
}

public ListenableFuture<LibraryResult<MediaItem>> onGetItemOnHandler(
ControllerInfo browser, String mediaId) {
// onGetItem is defined to return a non-null result but it's implemented by applications,
// so we explicitly null-check the result to fail early if an app accidentally returns null.
return checkNotNull(
callback.onGetItem(instance, browser, mediaId), "onGetItem must return non-null future");
ListenableFuture<LibraryResult<MediaItem>> future =
callback.onGetLibraryRoot(instance, browser, params);
future.addListener(
() -> {
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
if (result != null) {
maybeUpdateLegacyErrorState(result);
}
},
MoreExecutors.directExecutor());
return future;
}

public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildrenOnHandler(
Expand All @@ -139,23 +140,35 @@ public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildrenOn
int page,
int pageSize,
@Nullable LibraryParams params) {
// onGetChildren is defined to return a non-null result but it's implemented by applications,
// so we explicitly null-check the result to fail early if an app accidentally returns null.
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
checkNotNull(
callback.onGetChildren(instance, browser, parentId, page, pageSize, params),
"onGetChildren must return non-null future");
callback.onGetChildren(instance, browser, parentId, page, pageSize, params);
future.addListener(
() -> {
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
if (result != null) {
maybeUpdateLegacyErrorState(result);
verifyResultItems(result, pageSize);
}
},
MoreExecutors.directExecutor());
return future;
}

public ListenableFuture<LibraryResult<MediaItem>> onGetItemOnHandler(
ControllerInfo browser, String mediaId) {
ListenableFuture<LibraryResult<MediaItem>> future =
callback.onGetItem(instance, browser, mediaId);
future.addListener(
() -> {
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
if (result != null) {
maybeUpdateLegacyErrorState(result);
}
},
MoreExecutors.directExecutor());
return future;
}

public ListenableFuture<LibraryResult<Void>> onSubscribeOnHandler(
ControllerInfo browser, String parentId, @Nullable LibraryParams params) {
ControllerCb controller = checkStateNotNull(browser.getControllerCb());
Expand Down Expand Up @@ -193,12 +206,8 @@ public ListenableFuture<LibraryResult<Void>> onSubscribeOnHandler(

public ListenableFuture<LibraryResult<Void>> onUnsubscribeOnHandler(
ControllerInfo browser, String parentId) {
// onUnsubscribe is defined to return a non-null result but it's implemented by applications,
// so we explicitly null-check the result to fail early if an app accidentally returns null.
ListenableFuture<LibraryResult<Void>> future =
checkNotNull(
callback.onUnsubscribe(instance, browser, parentId),
"onUnsubscribe must return non-null future");
callback.onUnsubscribe(instance, browser, parentId);

future.addListener(
() -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId),
Expand All @@ -209,11 +218,17 @@ public ListenableFuture<LibraryResult<Void>> onUnsubscribeOnHandler(

public ListenableFuture<LibraryResult<Void>> onSearchOnHandler(
ControllerInfo browser, String query, @Nullable LibraryParams params) {
// onSearch is defined to return a non-null result but it's implemented by applications,
// so we explicitly null-check the result to fail early if an app accidentally returns null.
return checkNotNull(
callback.onSearch(instance, browser, query, params),
"onSearch must return non-null future");
ListenableFuture<LibraryResult<Void>> future =
callback.onSearch(instance, browser, query, params);
future.addListener(
() -> {
@Nullable LibraryResult<Void> result = tryGetFutureResult(future);
if (result != null) {
maybeUpdateLegacyErrorState(result);
}
},
MoreExecutors.directExecutor());
return future;
}

public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResultOnHandler(
Expand All @@ -222,17 +237,13 @@ public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResu
int page,
int pageSize,
@Nullable LibraryParams params) {
// onGetSearchResult is defined to return a non-null result but it's implemented by
// applications, so we explicitly null-check the result to fail early if an app accidentally
// returns null.
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
checkNotNull(
callback.onGetSearchResult(instance, browser, query, page, pageSize, params),
"onGetSearchResult must return non-null future");
callback.onGetSearchResult(instance, browser, query, page, pageSize, params);
future.addListener(
() -> {
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
if (result != null) {
maybeUpdateLegacyErrorState(result);
verifyResultItems(result, pageSize);
}
},
Expand Down Expand Up @@ -277,6 +288,27 @@ private boolean isSubscribed(ControllerCb callback, String parentId) {
return true;
}

private void maybeUpdateLegacyErrorState(LibraryResult<?> result) {
PlayerWrapper playerWrapper = getPlayerWrapper();
if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED
&& result.params != null
&& result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) {
// Mapping this error to the legacy error state provides backwards compatibility for the
// Automotive OS sign-in.
MediaSessionCompat mediaSessionCompat = getSessionCompat();
if (playerWrapper.getLegacyStatusCode() != RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED) {
playerWrapper.setLegacyErrorStatus(
ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT,
getContext().getString(R.string.authentication_required),
result.params.extras);
mediaSessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat());
}
} else if (playerWrapper.getLegacyStatusCode() != RESULT_SUCCESS) {
playerWrapper.clearLegacyErrorStatus();
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
}
}

@Nullable
private static <T> T tryGetFutureResult(Future<T> future) {
checkState(future.isDone());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
*/
package androidx.media3.session;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaConstants.STATUS_CODE_SUCCESS_COMPAT;

import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
Expand Down Expand Up @@ -52,8 +55,46 @@
*/
/* package */ class PlayerWrapper extends ForwardingPlayer {

private int legacyStatusCode;
@Nullable private String legacyErrorMessage;
@Nullable private Bundle legacyErrorExtras;

public PlayerWrapper(Player player) {
super(player);
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
}

/**
* Sets the legacy error code.
*
* <p>This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR}
* and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link
* PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments.
*
* <p>Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual
* playback state reflecting the player.
*
* @param errorCode The legacy error code.
* @param errorMessage The legacy error message.
* @param extras The extras.
*/
public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) {
checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT);
legacyStatusCode = errorCode;
legacyErrorMessage = errorMessage;
legacyErrorExtras = extras;
}

/** Returns the legacy status code. */
public int getLegacyStatusCode() {
return legacyStatusCode;
}

/** Clears the legacy error status. */
public void clearLegacyErrorStatus() {
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
legacyErrorMessage = null;
legacyErrorExtras = null;
}

@Override
Expand Down Expand Up @@ -702,6 +743,19 @@ public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
}

public PlaybackStateCompat createPlaybackStateCompat() {
if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) {
return new PlaybackStateCompat.Builder()
.setState(
PlaybackStateCompat.STATE_ERROR,
/* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
/* playbackSpeed= */ 0,
/* updateTime= */ SystemClock.elapsedRealtime())
.setActions(0)
.setBufferedPosition(0)
.setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage))
.setExtras(checkNotNull(legacyErrorExtras))
.build();
}
@Nullable PlaybackException playerError = getPlayerError();
int state =
MediaUtils.convertToPlaybackStateCompatState(
Expand Down
1 change: 1 addition & 0 deletions libraries/session/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@
<string name="media3_controls_seek_back_description">Seek back</string>
<!-- Accessibility description for a 'seek forward' or 'fast-forward' button on a media notification. [CHAR LIMIT=NONE] -->
<string name="media3_controls_seek_forward_description">Seek forward</string>
<string name="authentication_required">Authentication required</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class MediaBrowserConstants {
public static final String PARENT_ID_LONG_LIST = "parent_id_long_list";
public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children";
public static final String PARENT_ID_ERROR = "parent_id_error";
public static final String PARENT_ID_AUTH_EXPIRED_ERROR = "parent_auth_expired_error";
public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL =
"parent_auth_expired_error_label";

public static final List<String> GET_CHILDREN_RESULT = new ArrayList<>();
public static final int CHILDREN_COUNT = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN;
Expand Down Expand Up @@ -59,6 +61,7 @@
import android.support.v4.media.MediaBrowserCompat.SearchCallback;
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
Expand Down Expand Up @@ -301,6 +304,47 @@ public void onChildrenLoaded(String parentId, List<MediaItem> children) {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
}

@Test
public void getChildren_authErrorResult() throws InterruptedException {
String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR;
connectAndWait();
CountDownLatch errorLatch = new CountDownLatch(1);
browserCompat.subscribe(
testParentId,
new SubscriptionCallback() {
@Override
public void onError(String parentId) {
assertThat(parentId).isEqualTo(testParentId);
errorLatch.countDown();
}
});
assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(lastReportedPlaybackStateCompat).isNotNull();
assertThat(lastReportedPlaybackStateCompat.getState())
.isEqualTo(PlaybackStateCompat.STATE_ERROR);
assertThat(
lastReportedPlaybackStateCompat
.getExtras()
.getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT))
.isEqualTo(PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL);

CountDownLatch successLatch = new CountDownLatch(1);
browserCompat.subscribe(
PARENT_ID,
new SubscriptionCallback() {
@Override
public void onChildrenLoaded(String parentId, List<MediaItem> children) {
assertThat(parentId).isEqualTo(PARENT_ID);
successLatch.countDown();
}
});
assertThat(successLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
// Any successful calls remove the error state,
assertThat(lastReportedPlaybackStateCompat.getState())
.isNotEqualTo(PlaybackStateCompat.STATE_ERROR);
assertThat(lastReportedPlaybackStateCompat.getExtras()).isNull();
}

@Test
public void getChildren_emptyResult() throws InterruptedException {
String testParentId = PARENT_ID_NO_CHILDREN;
Expand Down

0 comments on commit 3ac7e0e

Please sign in to comment.