/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.datasource;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.List;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.internal.Objects;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Supplier;
/**
* {@link DataSource} supplier that provides a data source which forwards results of the underlying
* data sources with the increasing quality.
*
* <p>Data sources are obtained in order. The first data source in array is considered to be of the
* highest quality. The first data source to provide an result gets forwarded until one of the
* higher quality data sources provides its final image at which point that data source gets
* forwarded (and so on). That being said, only the first data source to provide an result is
* streamed.
*
* <p>Outcome (success/failure) of the data source provided by this supplier is determined by the
* outcome of the highest quality data source (the first data source in the array).
*/
@ThreadSafe
public class IncreasingQualityDataSourceSupplier<T> implements Supplier<DataSource<T>> {
private final List<Supplier<DataSource<T>>> mDataSourceSuppliers;
private IncreasingQualityDataSourceSupplier(List<Supplier<DataSource<T>>> dataSourceSuppliers) {
Preconditions.checkArgument(!dataSourceSuppliers.isEmpty(), "List of suppliers is empty!");
mDataSourceSuppliers = dataSourceSuppliers;
}
/**
* Creates a new data source supplier with increasing-quality strategy.
* <p>Note: for performance reasons the list doesn't get cloned, so the caller of this method
* should not modify the list once passed in here.
* @param dataSourceSuppliers list of underlying suppliers
*/
public static <T> IncreasingQualityDataSourceSupplier<T> create(
List<Supplier<DataSource<T>>> dataSourceSuppliers) {
return new IncreasingQualityDataSourceSupplier<T>(dataSourceSuppliers);
}
@Override
public DataSource<T> get() {
return new IncreasingQualityDataSource();
}
@Override
public int hashCode() {
return mDataSourceSuppliers.hashCode();
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof IncreasingQualityDataSourceSupplier)) {
return false;
}
IncreasingQualityDataSourceSupplier that = (IncreasingQualityDataSourceSupplier) other;
return Objects.equal(this.mDataSourceSuppliers, that.mDataSourceSuppliers);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("list", mDataSourceSuppliers)
.toString();
}
@ThreadSafe
private class IncreasingQualityDataSource extends AbstractDataSource<T> {
@GuardedBy("IncreasingQualityDataSource.this")
private @Nullable ArrayList<DataSource<T>> mDataSources;
@GuardedBy("IncreasingQualityDataSource.this")
private int mIndexOfDataSourceWithResult;
public IncreasingQualityDataSource() {
final int n = mDataSourceSuppliers.size();
mIndexOfDataSourceWithResult = n;
mDataSources = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
DataSource<T> dataSource = mDataSourceSuppliers.get(i).get();
mDataSources.add(dataSource);
dataSource.subscribe(new InternalDataSubscriber(i), CallerThreadExecutor.getInstance());
// there's no point in creating data sources of lower quality
// if the data source of a higher quality has some result already
if (dataSource.hasResult()) {
break;
}
}
}
@Nullable
private synchronized DataSource<T> getDataSource(int i) {
return (mDataSources != null && i < mDataSources.size()) ? mDataSources.get(i) : null;
}
@Nullable
private synchronized DataSource<T> getAndClearDataSource(int i) {
return (mDataSources != null && i < mDataSources.size()) ? mDataSources.set(i, null) : null;
}
@Nullable
private synchronized DataSource<T> getDataSourceWithResult() {
return getDataSource(mIndexOfDataSourceWithResult);
}
@Override
@Nullable
public synchronized T getResult() {
DataSource<T> dataSourceWithResult = getDataSourceWithResult();
return (dataSourceWithResult != null) ? dataSourceWithResult.getResult() : null;
}
@Override
public synchronized boolean hasResult() {
DataSource<T> dataSourceWithResult = getDataSourceWithResult();
return (dataSourceWithResult != null) && dataSourceWithResult.hasResult();
}
@Override
public boolean close() {
ArrayList<DataSource<T>> dataSources;
synchronized (IncreasingQualityDataSource.this) {
// it's fine to call {@code super.close()} within a synchronized block because we don't
// implement {@link #closeResult()}, but perform result closing ourselves.
if (!super.close()) {
return false;
}
dataSources = mDataSources;
mDataSources = null;
}
if (dataSources != null) {
for (int i = 0; i < dataSources.size(); i++) {
closeSafely(dataSources.get(i));
}
}
return true;
}
private void onDataSourceNewResult(int index, DataSource<T> dataSource) {
maybeSetIndexOfDataSourceWithResult(index, dataSource, dataSource.isFinished());
// If the data source with the new result is our {@code mIndexOfDataSourceWithResult},
// we have to notify our subscribers about the new result.
if (dataSource == getDataSourceWithResult()) {
setResult(null, (index == 0) && dataSource.isFinished());
}
}
private void onDataSourceFailed(int index, DataSource<T> dataSource) {
closeSafely(tryGetAndClearDataSource(index, dataSource));
if (index == 0) {
setFailure(dataSource.getFailureCause());
}
}
private void maybeSetIndexOfDataSourceWithResult(
int index,
DataSource<T> dataSource,
boolean isFinished) {
int oldIndexOfDataSourceWithResult;
int newIndexOfDataSourceWithResult;
synchronized (IncreasingQualityDataSource.this) {
oldIndexOfDataSourceWithResult = mIndexOfDataSourceWithResult;
newIndexOfDataSourceWithResult = mIndexOfDataSourceWithResult;
if (dataSource != getDataSource(index) || index == mIndexOfDataSourceWithResult) {
return;
}
// If we didn't have any result so far, we got one now, so we'll set
// {@code mIndexOfDataSourceWithResult} to point to the data source with result.
// If we did have a result which came from another data source,
// we'll only set {@code mIndexOfDataSourceWithResult} to point to the current data source
// if it has finished (i.e. the new result is final), and is of higher quality.
if (getDataSourceWithResult() == null ||
(isFinished && index < mIndexOfDataSourceWithResult)) {
newIndexOfDataSourceWithResult = index;
mIndexOfDataSourceWithResult = index;
}
}
// close data sources of lower quality than the one with the result
for (int i = oldIndexOfDataSourceWithResult; i > newIndexOfDataSourceWithResult; i--) {
closeSafely(getAndClearDataSource(i));
}
}
@Nullable
private synchronized DataSource<T> tryGetAndClearDataSource(int i, DataSource<T> dataSource) {
if (dataSource == getDataSourceWithResult()) {
return null;
}
if (dataSource == getDataSource(i)) {
return getAndClearDataSource(i);
}
return dataSource;
}
private void closeSafely(DataSource<T> dataSource) {
if (dataSource != null) {
dataSource.close();
}
}
private class InternalDataSubscriber implements DataSubscriber<T> {
private int mIndex;
public InternalDataSubscriber(int index) {
mIndex = index;
}
@Override
public void onNewResult(DataSource<T> dataSource) {
if (dataSource.hasResult()) {
IncreasingQualityDataSource.this.onDataSourceNewResult(mIndex, dataSource);
} else if (dataSource.isFinished()) {
IncreasingQualityDataSource.this.onDataSourceFailed(mIndex, dataSource);
}
}
@Override
public void onFailure(DataSource<T> dataSource) {
IncreasingQualityDataSource.this.onDataSourceFailed(mIndex, dataSource);
}
@Override
public void onCancellation(DataSource<T> dataSource) {
}
@Override
public void onProgressUpdate(DataSource<T> dataSource) {
if (mIndex == 0) {
IncreasingQualityDataSource.this.setProgress(dataSource.getProgress());
}
}
}
}
}