Implement Bundleable for Timeline

PiperOrigin-RevId: 362474276
This commit is contained in:
gyumin 2021-03-12 09:45:08 +00:00 committed by Ian Baker
parent 3f4f2f90b5
commit f8fb9dd606
8 changed files with 430 additions and 10 deletions

View File

@ -32,6 +32,7 @@ project.ext {
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxCoreVersion = '1.3.2'
androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.2.1'
androidxMedia2Version = '1.1.2'
@ -42,6 +43,7 @@ project.ext {
androidxTestRunnerVersion = '1.3.0'
androidxTestRulesVersion = '1.3.0'
androidxTestServicesStorageVersion = '1.3.0'
androidxTestTruthVersion = '1.3.0'
truthVersion = '1.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {

View File

@ -26,6 +26,7 @@ dependencies {
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.core:core:' + androidxCoreVersion
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -0,0 +1,125 @@
/*
* 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.android.exoplayer2.util.Assertions.checkNotNull;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.List;
/**
* A {@link Binder} to transfer a list of {@link Bundle Bundles} across processes by splitting the
* list into multiple transactions.
*
* <p>Note: Using this class causes synchronous binder calls in the opposite direction regardless of
* the "oneway" property.
*
* <p>Example usage:
*
* <pre>{@code
* // Sender
* List<Bundle> list = ...;
* IBinder binder = new BundleListRetriever(list);
* Bundle bundle = new Bundle();
* bundle.putBinder("list", binder);
*
* // Receiver
* Bundle bundle = ...; // Received from the sender
* IBinder binder = bundle.getBinder("list");
* List<Bundle> list = BundleListRetriever.getList(binder);
* }</pre>
*/
public final class BundleListRetriever extends Binder {
// Soft limit of an IPC buffer size
private static final int SUGGESTED_MAX_IPC_SIZE =
Util.SDK_INT >= 30 ? IBinder.getSuggestedMaxIpcSizeBytes() : 64 * 1024;
private static final int REPLY_END_OF_LIST = 0;
private static final int REPLY_CONTINUE = 1;
private static final int REPLY_BREAK = 2;
private final ImmutableList<Bundle> list;
/** Creates a {@link Binder} to send a list of {@link Bundle Bundles} to another process. */
public BundleListRetriever(List<Bundle> list) {
this.list = ImmutableList.copyOf(list);
}
@Override
protected boolean onTransact(int code, Parcel data, @Nullable Parcel reply, int flags)
throws RemoteException {
if (code != FIRST_CALL_TRANSACTION) {
return super.onTransact(code, data, reply, flags);
}
if (reply == null) {
return false;
}
int count = list.size();
int index = data.readInt();
while (index < count && reply.dataSize() < SUGGESTED_MAX_IPC_SIZE) {
reply.writeInt(REPLY_CONTINUE);
reply.writeBundle(list.get(index));
index++;
}
reply.writeInt(index < count ? REPLY_BREAK : REPLY_END_OF_LIST);
return true;
}
/**
* Gets a list of {@link Bundle Bundles} from a {@link BundleListRetriever}.
*
* @param binder A binder interface backed by {@link BundleListRetriever}.
* @return The list of {@link Bundle Bundles}.
*/
public static ImmutableList<Bundle> getList(IBinder binder) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
int index = 0;
int replyCode = REPLY_CONTINUE;
while (replyCode != REPLY_END_OF_LIST) {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInt(index);
try {
binder.transact(FIRST_CALL_TRANSACTION, data, reply, /* flags= */ 0);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
while ((replyCode = reply.readInt()) == REPLY_CONTINUE) {
builder.add(checkNotNull(reply.readBundle()));
index++;
}
} finally {
reply.recycle();
data.recycle();
}
}
return builder.build();
}
}

View File

@ -19,16 +19,21 @@ import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.app.BundleCompat;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* A flexible representation of the structure of media. A timeline is able to represent the
@ -124,7 +129,7 @@ import java.lang.annotation.RetentionPolicy;
* <p>This case includes mid-roll ad groups, which are defined as part of the timeline's single
* period. The period can be queried for information about the ad groups and the ads they contain.
*/
public abstract class Timeline {
public abstract class Timeline implements Bundleable {
/**
* Holds information about a window in a {@link Timeline}. A window usually corresponds to one
@ -1245,4 +1250,148 @@ public abstract class Timeline {
}
return result;
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({FIELD_WINDOWS, FIELD_PERIODS})
private @interface FieldNumber {}
private static final int FIELD_WINDOWS = 0;
private static final int FIELD_PERIODS = 1;
/**
* {@inheritDoc}
*
* <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
* an instance restored by {@link #CREATOR} may have missing fields as described in {@link
* Window#toBundle()} and {@link Period#toBundle()}.
*/
@Override
public final Bundle toBundle() {
List<Bundle> windowBundles = new ArrayList<>();
int windowCount = getWindowCount();
for (int i = 0; i < windowCount; i++) {
Window window = new Window();
getWindow(i, window, /* defaultPositionProjectionUs= */ 0);
windowBundles.add(window.toBundle());
}
List<Bundle> periodBundles = new ArrayList<>();
int periodCount = getPeriodCount();
for (int i = 0; i < periodCount; i++) {
Period period = new Period();
getPeriod(i, period, /* setIds= */ false);
periodBundles.add(period.toBundle());
}
Bundle bundle = new Bundle();
BundleCompat.putBinder(
bundle, keyForField(FIELD_WINDOWS), new BundleListRetriever(windowBundles));
BundleCompat.putBinder(
bundle, keyForField(FIELD_PERIODS), new BundleListRetriever(periodBundles));
return bundle;
}
/**
* Object that can restore a {@link Timeline} from a {@link Bundle}.
*
* <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
* a restored instance may have missing fields as described in {@link Window#CREATOR} and {@link
* Period#CREATOR}.
*/
public static final Creator<Timeline> CREATOR = Timeline::fromBundle;
private static Timeline fromBundle(Bundle bundle) {
ImmutableList<Window> windows =
fromBundleListRetriever(
Window.CREATOR, BundleCompat.getBinder(bundle, keyForField(FIELD_WINDOWS)));
ImmutableList<Period> periods =
fromBundleListRetriever(
Period.CREATOR, BundleCompat.getBinder(bundle, keyForField(FIELD_PERIODS)));
return new RemotableTimeline(windows, periods);
}
private static <T extends Bundleable> ImmutableList<T> fromBundleListRetriever(
Creator<T> creator, @Nullable IBinder binder) {
if (binder == null) {
return ImmutableList.of();
}
ImmutableList.Builder<T> builder = new ImmutableList.Builder<>();
List<Bundle> bundleList = BundleListRetriever.getList(binder);
for (int i = 0; i < bundleList.size(); i++) {
builder.add(creator.fromBundle(bundleList.get(i)));
}
return builder.build();
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
/**
* A concrete class of {@link Timeline} to restore a {@link Timeline} instance from a {@link
* Bundle} sent by another process via {@link IBinder}.
*/
private static final class RemotableTimeline extends Timeline {
private final ImmutableList<Window> windows;
private final ImmutableList<Period> periods;
public RemotableTimeline(ImmutableList<Window> windows, ImmutableList<Period> periods) {
this.windows = windows;
this.periods = periods;
}
@Override
public int getWindowCount() {
return windows.size();
}
@Override
public Window getWindow(
int windowIndex, Window window, long ignoredDefaultPositionProjectionUs) {
Window w = windows.get(windowIndex);
window.set(
w.uid,
w.mediaItem,
w.manifest,
w.presentationStartTimeMs,
w.windowStartTimeMs,
w.elapsedRealtimeEpochOffsetMs,
w.isSeekable,
w.isDynamic,
w.liveConfiguration,
w.defaultPositionUs,
w.durationUs,
w.firstPeriodIndex,
w.lastPeriodIndex,
w.positionInFirstPeriodUs);
window.isPlaceholder = w.isPlaceholder;
return window;
}
@Override
public int getPeriodCount() {
return periods.size();
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean ignoredSetIds) {
Period p = periods.get(periodIndex);
return period.set(
p.id, p.uid, p.windowIndex, p.durationUs, p.positionInWindowUs, p.adPlaybackState);
}
@Override
public int getIndexOfPeriod(Object uid) {
throw new UnsupportedOperationException();
}
@Override
public Object getUidOfPeriod(int periodIndex) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.ext.truth.os.BundleSubject;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link BundleListRetriever}. */
@RunWith(AndroidJUnit4.class)
public class BundleListRetrieverTest {
@Test
public void getList_preservedLargeList() {
int count = 100_000;
List<Bundle> listBefore = new ArrayList<>();
for (int i = 0; i < count; i++) {
Bundle bundle = new Bundle();
bundle.putInt("i", i);
listBefore.add(bundle);
}
List<Bundle> listAfter = BundleListRetriever.getList(new BundleListRetriever(listBefore));
for (int i = 0; i < count; i++) {
Bundle bundle = listAfter.get(i);
BundleSubject.assertThat(bundle).integer("i").isEqualTo(i);
}
}
}

View File

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem.LiveConfiguration;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
@ -201,6 +202,41 @@ public class TimelineTest {
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
}
@Test
public void roundtripViaBundle_ofTimeline_yieldsEqualInstanceExceptIdsAndManifest() {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 2,
/* id= */ new Object(),
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 2,
/* defaultPositionUs= */ 22,
/* windowOffsetInFirstPeriodUs= */ 222,
AdPlaybackState.NONE,
new MediaItem.Builder().setMediaId("mediaId2").build()),
new TimelineWindowDefinition(
/* periodCount= */ 3,
/* id= */ new Object(),
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 3,
/* defaultPositionUs= */ 33,
/* windowOffsetInFirstPeriodUs= */ 333,
AdPlaybackState.NONE,
new MediaItem.Builder().setMediaId("mediaId3").build()));
Timeline restoredTimeline = Timeline.CREATOR.fromBundle(timeline.toBundle());
TimelineAsserts.assertEqualsExceptIdsAndManifest(
/* expectedTimeline= */ timeline, /* actualTimeline= */ restoredTimeline);
}
@Test
public void roundtripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() {
Timeline.Window window = new Timeline.Window();
@ -229,9 +265,8 @@ public class TimelineTest {
Timeline.Window restoredWindow = Timeline.Window.CREATOR.fromBundle(window.toBundle());
assertThat(restoredWindow.manifest).isNull();
window.uid = restoredWindow.uid;
window.manifest = null;
assertThat(restoredWindow).isEqualTo(window);
TimelineAsserts.assertWindowEqualsExceptUidAndManifest(
/* expectedWindow= */ window, /* actualWindow= */ restoredWindow);
}
@Test
@ -245,9 +280,10 @@ public class TimelineTest {
Timeline.Period restoredPeriod = Timeline.Period.CREATOR.fromBundle(period.toBundle());
period.id = null;
period.uid = null;
assertThat(restoredPeriod).isEqualTo(period);
assertThat(restoredPeriod.id).isNull();
assertThat(restoredPeriod.uid).isNull();
TimelineAsserts.assertPeriodEqualsExceptIds(
/* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod);
}
@SuppressWarnings("deprecation") // Populates the deprecated window.tag property.

View File

@ -17,6 +17,7 @@ dependencies {
api 'org.mockito:mockito-core:' + mockitoVersion
api 'androidx.test:core:' + androidxTestCoreVersion
api 'androidx.test.ext:junit:' + androidxTestJUnitVersion
api 'androidx.test.ext:truth:' + androidxTestTruthVersion
api 'junit:junit:' + junitVersion
api 'com.google.truth:truth:' + truthVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
@ -27,15 +28,13 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Unit test for {@link Timeline}. */
/** Assertion methods for {@link Timeline}. */
public final class TimelineAsserts {
private static final int[] REPEAT_MODES = {
Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL
};
private TimelineAsserts() {}
/** Assert that timeline is empty (i.e. has no windows or periods). */
public static void assertEmpty(Timeline timeline) {
assertWindowTags(timeline);
@ -173,4 +172,64 @@ public final class TimelineAsserts {
assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]);
}
}
/**
* Asserts that {@link Timeline timelines} are equal except {@link Window#uid}, {@link
* Window#manifest}, {@link Period#id}, and {@link Period#uid}.
*/
public static void assertEqualsExceptIdsAndManifest(
Timeline expectedTimeline, Timeline actualTimeline) {
assertThat(actualTimeline.getWindowCount()).isEqualTo(expectedTimeline.getWindowCount());
for (int i = 0; i < actualTimeline.getWindowCount(); i++) {
Window expectedWindow = new Window();
Window actualWindow = new Window();
assertWindowEqualsExceptUidAndManifest(
expectedTimeline.getWindow(i, expectedWindow, /* defaultPositionProjectionUs= */ 0),
actualTimeline.getWindow(i, actualWindow, /* defaultPositionProjectionUs= */ 0));
}
assertThat(actualTimeline.getPeriodCount()).isEqualTo(expectedTimeline.getPeriodCount());
for (int i = 0; i < actualTimeline.getPeriodCount(); i++) {
Period expectedPeriod = new Period();
Period actualPeriod = new Period();
assertPeriodEqualsExceptIds(
expectedTimeline.getPeriod(i, expectedPeriod, /* setIds= */ false),
actualTimeline.getPeriod(i, actualPeriod, /* setIds= */ false));
}
}
/**
* Asserts that {@link Window windows} are equal except {@link Window#uid} and {@link
* Window#manifest}.
*/
public static void assertWindowEqualsExceptUidAndManifest(
Window expectedWindow, Window actualWindow) {
Object uid = expectedWindow.uid;
@Nullable Object manifest = expectedWindow.manifest;
try {
expectedWindow.uid = actualWindow.uid;
expectedWindow.manifest = actualWindow.manifest;
assertThat(actualWindow).isEqualTo(expectedWindow);
} finally {
expectedWindow.uid = uid;
expectedWindow.manifest = manifest;
}
}
/**
* Asserts that {@link Period periods} are equal except {@link Period#id} and {@link Period#uid}.
*/
public static void assertPeriodEqualsExceptIds(Period expectedPeriod, Period actualPeriod) {
@Nullable Object id = expectedPeriod.id;
@Nullable Object uid = expectedPeriod.uid;
try {
expectedPeriod.id = actualPeriod.id;
expectedPeriod.uid = actualPeriod.uid;
assertThat(actualPeriod).isEqualTo(expectedPeriod);
} finally {
expectedPeriod.id = id;
expectedPeriod.uid = uid;
}
}
private TimelineAsserts() {}
}