FakeDataSource.java
/*
* Copyright (C) 2016 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 androidx.media3.test.utils;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.BaseDataSource;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSourceException;
import androidx.media3.datasource.DataSpec;
import androidx.media3.test.utils.FakeDataSet.FakeData;
import androidx.media3.test.utils.FakeDataSet.FakeData.Segment;
import java.io.IOException;
import java.util.ArrayList;
/**
* A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet}
* instance which determines the response to data access calls.
*/
@UnstableApi
public class FakeDataSource extends BaseDataSource {
/** Factory to create a {@link FakeDataSource}. */
public static class Factory implements DataSource.Factory {
protected FakeDataSet fakeDataSet;
protected boolean isNetwork;
public Factory() {
fakeDataSet = new FakeDataSet();
}
public final Factory setFakeDataSet(FakeDataSet fakeDataSet) {
this.fakeDataSet = fakeDataSet;
return this;
}
public final Factory setIsNetwork(boolean isNetwork) {
this.isNetwork = isNetwork;
return this;
}
@Override
public FakeDataSource createDataSource() {
return new FakeDataSource(fakeDataSet, isNetwork);
}
}
private final FakeDataSet fakeDataSet;
private final ArrayList<DataSpec> openedDataSpecs;
@Nullable private Uri uri;
private boolean openCalled;
private boolean sourceOpened;
@Nullable private FakeData fakeData;
private int currentSegmentIndex;
private long bytesRemaining;
public FakeDataSource() {
this(new FakeDataSet());
}
public FakeDataSource(FakeDataSet fakeDataSet) {
this(fakeDataSet, /* isNetwork= */ false);
}
public FakeDataSource(FakeDataSet fakeDataSet, boolean isNetwork) {
super(isNetwork);
Assertions.checkNotNull(fakeDataSet);
this.fakeDataSet = fakeDataSet;
this.openedDataSpecs = new ArrayList<>();
}
public final FakeDataSet getDataSet() {
return fakeDataSet;
}
@Override
public final long open(DataSpec dataSpec) throws IOException {
Assertions.checkState(!openCalled);
openCalled = true;
// DataSpec requires a matching close call even if open fails.
uri = dataSpec.uri;
openedDataSpecs.add(dataSpec);
transferInitializing(dataSpec);
FakeData fakeData = fakeDataSet.getData(dataSpec.uri.toString());
if (fakeData == null) {
throw new IOException("Data not found: " + dataSpec.uri);
}
this.fakeData = fakeData;
long totalLength = 0;
for (Segment segment : fakeData.getSegments()) {
totalLength += segment.length;
}
if (totalLength == 0) {
throw new IOException("Data is empty: " + dataSpec.uri);
}
if (dataSpec.position > totalLength) {
throw new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE);
}
// Scan through the segments, configuring them for the current read.
boolean findingCurrentSegmentIndex = true;
currentSegmentIndex = 0;
int scannedLength = 0;
for (Segment segment : fakeData.getSegments()) {
segment.bytesRead = (int) min(max(0, dataSpec.position - scannedLength), segment.length);
scannedLength += segment.length;
findingCurrentSegmentIndex &=
segment.isErrorSegment()
? segment.exceptionCleared
: (!segment.isActionSegment() && segment.bytesRead == segment.length);
if (findingCurrentSegmentIndex) {
currentSegmentIndex++;
}
}
sourceOpened = true;
transferStarted(dataSpec);
// Configure bytesRemaining, and return.
if (dataSpec.length == C.LENGTH_UNSET) {
bytesRemaining = totalLength - dataSpec.position;
return fakeData.isSimulatingUnknownLength() ? C.LENGTH_UNSET : bytesRemaining;
} else {
bytesRemaining = dataSpec.length;
return bytesRemaining;
}
}
@Override
public final int read(byte[] buffer, int offset, int length) throws IOException {
Assertions.checkState(sourceOpened);
while (true) {
FakeData fakeData = Util.castNonNull(this.fakeData);
if (currentSegmentIndex == fakeData.getSegments().size() || bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
Segment current = fakeData.getSegments().get(currentSegmentIndex);
if (current.isErrorSegment()) {
if (!current.exceptionCleared) {
current.exceptionThrown = true;
throw (IOException) Util.castNonNull(current.exception).fillInStackTrace();
} else {
currentSegmentIndex++;
}
} else if (current.isActionSegment()) {
currentSegmentIndex++;
Util.castNonNull(current.action).run();
} else {
// Read at most bytesRemaining.
length = (int) min(length, bytesRemaining);
// Do not allow crossing of the segment boundary.
length = min(length, current.length - current.bytesRead);
// Perform the read and return.
Assertions.checkArgument(buffer.length - offset >= length);
if (current.data != null) {
System.arraycopy(current.data, current.bytesRead, buffer, offset, length);
}
onDataRead(length);
bytesTransferred(length);
bytesRemaining -= length;
current.bytesRead += length;
if (current.bytesRead == current.length) {
currentSegmentIndex++;
}
return length;
}
}
}
@Override
@Nullable
public final Uri getUri() {
return uri;
}
@Override
public final void close() {
Assertions.checkState(openCalled);
openCalled = false;
uri = null;
if (fakeData != null && currentSegmentIndex < fakeData.getSegments().size()) {
Segment current = fakeData.getSegments().get(currentSegmentIndex);
if (current.isErrorSegment() && current.exceptionThrown) {
current.exceptionCleared = true;
}
}
if (sourceOpened) {
sourceOpened = false;
transferEnded();
}
fakeData = null;
onClosed();
}
/**
* Returns the {@link DataSpec} instances passed to {@link #open(DataSpec)} since the last call to
* this method.
*/
public final DataSpec[] getAndClearOpenedDataSpecs() {
DataSpec[] dataSpecs = openedDataSpecs.toArray(new DataSpec[0]);
openedDataSpecs.clear();
return dataSpecs;
}
/** Returns whether the data source is currently opened. */
public final boolean isOpened() {
return sourceOpened;
}
/** Called when data is being read. */
protected void onDataRead(int bytesRead) throws IOException {
// Do nothing. Can be overridden.
}
/** Called when the source is closed. */
protected void onClosed() {
// Do nothing. Can be overridden.
}
}