/* * 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 com.google.android.exoplayer2.testutil; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; /** * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} * instance which determines the response to data access calls. * * <p>Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link * FakeDataSet#newDefaultData()}. * * <p>{@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link * FakeData} instance which can be used to define specific results during {@link #read(byte[], int, * int)} calls. * * <p>The data that will be read from the source can be constructed by calling {@link * FakeData#appendReadData(byte[])} Calls to {@link #read(byte[], int, int)} will not span the * boundaries between arrays passed to successive calls, and hence the boundaries control the * positions at which read requests to the source may only be partially satisfied. * * <p>Errors can be inserted by calling {@link FakeData#appendReadError(IOException)}. An inserted * error will be thrown from the first call to {@link #read(byte[], int, int)} that attempts to read * from the corresponding position, and from all subsequent calls to {@link #read(byte[], int, int)} * until the source is closed. If the source is closed and re-opened having encountered an error, * that error will not be thrown again. * * <p>Example usage: * * <pre> * // Create a FakeDataSource then add default data and two FakeData * // "test_file" throws an IOException when tried to be read until closed and reopened. * FakeDataSource fakeDataSource = new FakeDataSource(); * fakeDataSource.getDataSet() * .newDefaultData() * .appendReadData(defaultData) * .endData() * .setData("http:///1", data1) * .newData("test_file") * .appendReadError(new IOException()) * .appendReadData(data2); * // No need to call endData at the end * </pre> */ public final class FakeDataSource implements DataSource { private final FakeDataSet fakeDataSet; private final ArrayList<DataSpec> openedDataSpecs; private Uri uri; private boolean opened; private FakeData fakeData; private int currentSegmentIndex; private long bytesRemaining; public static Factory newFactory(final FakeDataSet fakeDataSet) { return new Factory() { @Override public DataSource createDataSource() { return new FakeDataSource(fakeDataSet); } }; } public FakeDataSource() { this(new FakeDataSet()); } public FakeDataSource(FakeDataSet fakeDataSet) { this.fakeDataSet = fakeDataSet; this.openedDataSpecs = new ArrayList<>(); } public FakeDataSet getDataSet() { return fakeDataSet; } @Override public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(!opened); // DataSpec requires a matching close call even if open fails. opened = true; uri = dataSpec.uri; openedDataSpecs.add(dataSpec); fakeData = fakeDataSet.getData(uri.toString()); if (fakeData == null) { throw new IOException("Data not found: " + dataSpec.uri); } long totalLength = 0; for (Segment segment : fakeData.segments) { totalLength += segment.length; } if (totalLength == 0) { throw new IOException("Data is empty: " + dataSpec.uri); } // If the source knows that the request is unsatisfiable then fail. if (dataSpec.position >= totalLength || (dataSpec.length != C.LENGTH_UNSET && (dataSpec.position + dataSpec.length > totalLength))) { throw new DataSourceException(DataSourceException.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.segments) { segment.bytesRead = (int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length); scannedLength += segment.length; findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared : segment.bytesRead == segment.length; if (findingCurrentSegmentIndex) { currentSegmentIndex++; } } // Configure bytesRemaining, and return. if (dataSpec.length == C.LENGTH_UNSET) { bytesRemaining = totalLength - dataSpec.position; return fakeData.simulateUnknownLength ? C.LENGTH_UNSET : bytesRemaining; } else { bytesRemaining = dataSpec.length; return bytesRemaining; } } @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { Assertions.checkState(opened); while (true) { if (currentSegmentIndex == fakeData.segments.size() || bytesRemaining == 0) { return C.RESULT_END_OF_INPUT; } Segment current = fakeData.segments.get(currentSegmentIndex); if (current.isErrorSegment()) { if (!current.exceptionCleared) { current.exceptionThrown = true; throw (IOException) current.exception.fillInStackTrace(); } else { currentSegmentIndex++; } } else { // Read at most bytesRemaining. readLength = (int) Math.min(readLength, bytesRemaining); // Do not allow crossing of the segment boundary. readLength = Math.min(readLength, current.length - current.bytesRead); // Perform the read and return. System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); bytesRemaining -= readLength; current.bytesRead += readLength; if (current.bytesRead == current.length) { currentSegmentIndex++; } return readLength; } } } @Override public Uri getUri() { return uri; } @Override public void close() throws IOException { Assertions.checkState(opened); opened = false; uri = null; if (currentSegmentIndex < fakeData.segments.size()) { Segment current = fakeData.segments.get(currentSegmentIndex); if (current.isErrorSegment() && current.exceptionThrown) { current.exceptionCleared = true; } } fakeData = null; } /** * Returns the {@link DataSpec} instances passed to {@link #open(DataSpec)} since the last call to * this method. */ public DataSpec[] getAndClearOpenedDataSpecs() { DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()]; openedDataSpecs.toArray(dataSpecs); openedDataSpecs.clear(); return dataSpecs; } private static class Segment { public final IOException exception; public final byte[] data; public final int length; private boolean exceptionThrown; private boolean exceptionCleared; private int bytesRead; public Segment(byte[] data, IOException exception) { this.data = data; this.exception = exception; length = data != null ? data.length : 0; } public boolean isErrorSegment() { return exception != null; } } /** Container of fake data to be served by a {@link FakeDataSource}. */ public static final class FakeData { /** Uri of the data or null if this is the default FakeData. */ public final String uri; private final ArrayList<Segment> segments; private final FakeDataSet dataSet; private boolean simulateUnknownLength; private FakeData(FakeDataSet dataSet, String uri) { this.uri = uri; this.segments = new ArrayList<>(); this.dataSet = dataSet; } /** Returns the {@link FakeDataSet} this FakeData belongs to. */ public FakeDataSet endData() { return dataSet; } /** * When set, {@link FakeDataSource#open(DataSpec)} will behave as though the source is unable to * determine the length of the underlying data. Hence the return value will always be equal to * the {@link DataSpec#length} of the argument, including the case where the length is equal to * {@link C#LENGTH_UNSET}. */ public FakeData setSimulateUnknownLength(boolean simulateUnknownLength) { this.simulateUnknownLength = simulateUnknownLength; return this; } /** * Appends to the underlying data. */ public FakeData appendReadData(byte[] data) { Assertions.checkState(data != null && data.length > 0); segments.add(new Segment(data, null)); return this; } /** * Appends an error in the underlying data. */ public FakeData appendReadError(IOException exception) { segments.add(new Segment(null, exception)); return this; } /** Returns the whole data added by {@link #appendReadData(byte[])}. */ public byte[] getData() { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); for (Segment segment : segments) { if (segment.data != null) { try { outputStream.write(segment.data); } catch (IOException e) { throw new IllegalStateException(e); } } } return outputStream.toByteArray(); } } /** A set of {@link FakeData} instances. */ public static final class FakeDataSet { private final HashMap<String, FakeData> dataMap; private FakeData defaultData; public FakeDataSet() { dataMap = new HashMap<>(); } public FakeData newDefaultData() { defaultData = new FakeData(this, null); return defaultData; } public FakeData newData(String uri) { FakeData data = new FakeData(this, uri); dataMap.put(uri, data); return data; } public FakeDataSet setData(String uri, byte[] data) { return newData(uri).appendReadData(data).endData(); } public FakeData getData(String uri) { FakeData data = dataMap.get(uri); return data != null ? data : defaultData; } public ArrayList<FakeData> getAllData() { ArrayList<FakeData> fakeDatas = new ArrayList<>(dataMap.values()); if (defaultData != null) { fakeDatas.add(defaultData); } return fakeDatas; } } }