diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
index 93f66123fc..4d9488bce2 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import android.os.Bundle;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.CheckResult;
@@ -29,7 +31,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Thrown when a non locally recoverable playback failure occurs. */
-public final class ExoPlaybackException extends Exception {
+public final class ExoPlaybackException extends Exception implements Bundleable {
/**
* The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER}
@@ -378,4 +380,136 @@ public final class ExoPlaybackException extends Exception {
}
return message;
}
+
+ // Bundleable implementation.
+ // TODO(b/145954241): Revisit bundling fields when this class is split for Player and ExoPlayer.
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_MESSAGE,
+ FIELD_TYPE,
+ FIELD_RENDERER_NAME,
+ FIELD_RENDERER_INDEX,
+ FIELD_RENDERER_FORMAT,
+ FIELD_RENDERER_FORMAT_SUPPORT,
+ FIELD_TIME_STAMP_MS,
+ FIELD_IS_RECOVERABLE,
+ FIELD_CAUSE_CLASS_NAME,
+ FIELD_CAUSE_MESSAGE
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MESSAGE = 0;
+ private static final int FIELD_TYPE = 1;
+ private static final int FIELD_RENDERER_NAME = 2;
+ private static final int FIELD_RENDERER_INDEX = 3;
+ private static final int FIELD_RENDERER_FORMAT = 4;
+ private static final int FIELD_RENDERER_FORMAT_SUPPORT = 5;
+ private static final int FIELD_TIME_STAMP_MS = 6;
+ private static final int FIELD_IS_RECOVERABLE = 7;
+ private static final int FIELD_CAUSE_CLASS_NAME = 8;
+ private static final int FIELD_CAUSE_MESSAGE = 9;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #mediaPeriodId} field. The {@link #mediaPeriodId} of an instance
+ * restored by {@link #CREATOR} will always be {@code null}.
+ */
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(keyForField(FIELD_MESSAGE), getMessage());
+ bundle.putInt(keyForField(FIELD_TYPE), type);
+ bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName);
+ bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex);
+ bundle.putParcelable(keyForField(FIELD_RENDERER_FORMAT), rendererFormat);
+ bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport);
+ bundle.putLong(keyForField(FIELD_TIME_STAMP_MS), timestampMs);
+ bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable);
+ if (cause != null) {
+ bundle.putString(keyForField(FIELD_CAUSE_CLASS_NAME), cause.getClass().getName());
+ bundle.putString(keyForField(FIELD_CAUSE_MESSAGE), cause.getMessage());
+ }
+ return bundle;
+ }
+
+ /** Object that can restore {@link ExoPlaybackException} from a {@link Bundle}. */
+ public static final Creator CREATOR = ExoPlaybackException::fromBundle;
+
+ private static ExoPlaybackException fromBundle(Bundle bundle) {
+ int type = bundle.getInt(keyForField(FIELD_TYPE), /* defaultValue= */ TYPE_UNEXPECTED);
+ @Nullable String rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME));
+ int rendererIndex =
+ bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET);
+ @Nullable Format rendererFormat = bundle.getParcelable(keyForField(FIELD_RENDERER_FORMAT));
+ int rendererFormatSupport =
+ bundle.getInt(
+ keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED);
+ long timestampMs =
+ bundle.getLong(
+ keyForField(FIELD_TIME_STAMP_MS), /* defaultValue= */ SystemClock.elapsedRealtime());
+ boolean isRecoverable =
+ bundle.getBoolean(keyForField(FIELD_IS_RECOVERABLE), /* defaultValue= */ false);
+ @Nullable String message = bundle.getString(keyForField(FIELD_MESSAGE));
+ if (message == null) {
+ message =
+ deriveMessage(
+ type,
+ /* customMessage= */ null,
+ rendererName,
+ rendererIndex,
+ rendererFormat,
+ rendererFormatSupport);
+ }
+
+ @Nullable String causeClassName = bundle.getString(keyForField(FIELD_CAUSE_CLASS_NAME));
+ @Nullable String causeMessage = bundle.getString(keyForField(FIELD_CAUSE_MESSAGE));
+ @Nullable Throwable cause = null;
+ if (!TextUtils.isEmpty(causeClassName)) {
+ final Class> clazz;
+ try {
+ clazz =
+ Class.forName(
+ causeClassName,
+ /* initialize= */ true,
+ ExoPlaybackException.class.getClassLoader());
+ if (Throwable.class.isAssignableFrom(clazz)) {
+ cause = createThrowable(clazz, causeMessage);
+ }
+ } catch (Throwable e) {
+ // Intentionally catch Throwable to catch both Exception and Error.
+ cause = createRemoteException(causeMessage);
+ }
+ }
+
+ return new ExoPlaybackException(
+ message,
+ cause,
+ type,
+ rendererName,
+ rendererIndex,
+ rendererFormat,
+ rendererFormatSupport,
+ /* mediaPeriodId= */ null,
+ timestampMs,
+ isRecoverable);
+ }
+
+ // Creates a new {@link Throwable} with possibly @{code null} message.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static Throwable createThrowable(Class> throwableClazz, @Nullable String message)
+ throws Exception {
+ return (Throwable) throwableClazz.getConstructor(String.class).newInstance(message);
+ }
+
+ // Creates a new {@link RemoteException} with possibly {@code null} message.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static RemoteException createRemoteException(@Nullable String message) {
+ return new RemoteException(message);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/ExoPlaybackExceptionTest.java b/library/common/src/test/java/com/google/android/exoplayer2/ExoPlaybackExceptionTest.java
new file mode 100644
index 0000000000..0a12305059
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/ExoPlaybackExceptionTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.RemoteException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.util.Util;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link ExoPlaybackException}. */
+@RunWith(AndroidJUnit4.class)
+public class ExoPlaybackExceptionTest {
+
+ @Test
+ public void roundtripViaBundle_ofExoPlaybackExceptionTypeRemote_yieldsEqualInstance() {
+ ExoPlaybackException before = ExoPlaybackException.createForRemote(/* message= */ "test");
+ ExoPlaybackException after = ExoPlaybackException.CREATOR.fromBundle(before.toBundle());
+ assertThat(areEqual(before, after)).isTrue();
+ }
+
+ @Test
+ public void roundtripViaBundle_ofExoPlaybackExceptionTypeRenderer_yieldsEqualInstance() {
+ ExoPlaybackException before =
+ ExoPlaybackException.createForRenderer(
+ new IllegalStateException("ExoPlaybackExceptionTest"),
+ /* rendererName= */ "rendererName",
+ /* rendererIndex= */ 123,
+ /* rendererFormat= */ new Format.Builder().setCodecs("anyCodec").build(),
+ /* rendererFormatSupport= */ C.FORMAT_UNSUPPORTED_SUBTYPE,
+ /* isRecoverable= */ true);
+
+ ExoPlaybackException after = ExoPlaybackException.CREATOR.fromBundle(before.toBundle());
+ assertThat(areEqual(before, after)).isTrue();
+ }
+
+ @Test
+ public void
+ roundtripViaBundle_ofExoPlaybackExceptionTypeRendererWithPrivateCause_yieldsRemoteExceptionWithSameMessage() {
+ ExoPlaybackException before =
+ ExoPlaybackException.createForRenderer(
+ new Exception(/* message= */ "anonymous exception that class loader cannot know") {});
+ ExoPlaybackException after = ExoPlaybackException.CREATOR.fromBundle(before.toBundle());
+
+ assertThat(after.getCause()).isInstanceOf(RemoteException.class);
+ assertThat(after.getCause()).hasMessageThat().isEqualTo(before.getCause().getMessage());
+ }
+
+ private static boolean areEqual(ExoPlaybackException a, ExoPlaybackException b) {
+ if (a == null || b == null) {
+ return a == b;
+ }
+ return Util.areEqual(a.getMessage(), b.getMessage())
+ && a.type == b.type
+ && Util.areEqual(a.rendererName, b.rendererName)
+ && a.rendererIndex == b.rendererIndex
+ && Util.areEqual(a.rendererFormat, b.rendererFormat)
+ && a.rendererFormatSupport == b.rendererFormatSupport
+ && a.timestampMs == b.timestampMs
+ && a.isRecoverable == b.isRecoverable
+ && areEqual(a.getCause(), b.getCause());
+ }
+
+ private static boolean areEqual(Throwable a, Throwable b) {
+ if (a == null || b == null) {
+ return a == b;
+ }
+ return a.getClass() == b.getClass() && Util.areEqual(a.getMessage(), b.getMessage());
+ }
+}