/*
* 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.imagepipeline.producers;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.Executor;
import android.os.SystemClock;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.memory.ByteArrayPool;
import com.facebook.imagepipeline.memory.PooledByteBuffer;
import com.facebook.imagepipeline.memory.PooledByteBufferFactory;
import com.facebook.imagepipeline.memory.PooledByteBufferOutputStream;
/**
* A producer to actually fetch images from the network.
*
* <p> Downloaded bytes are passed to the consumer as they are downloaded, but not more often than
* {@link #TIME_BETWEEN_PARTIAL_RESULTS_MS}.
* <p>Implementations should subclass this to make use of the network stack they are using.
* Use {@link HttpURLConnectionNetworkFetchProducer} as a model.
*
* <p>Most implementations will only need to override
* {@link #newRequestState(Consumer, ProducerContext)} and {@link #fetchImage(NfpRequestState)}.
*
* <p>It is strongly recommended that implementations use an {@link Executor} in their fetchImage
* method to execute the network request on a different thread.
*
* <p>When the fetch from the network fails or is cancelled, the subclass is responsible for
* calling {@link #onCancellation(NfpRequestState, Map)} or
* {@link #onFailure(NfpRequestState, Throwable, Map)}. If these are not called, the
* rest of the pipeline will not know that the image has failed to load and the application
* may not behave properly.
*
* @param <RS> The type to store all request-scoped data. NfpRequestState can be used or extended.
*/
public abstract class NetworkFetchProducer<RS extends NfpRequestState>
implements Producer<CloseableReference<PooledByteBuffer>> {
@VisibleForTesting static final String PRODUCER_NAME = "NetworkFetchProducer";
public static final String INTERMEDIATE_RESULT_PRODUCER_EVENT = "intermediate_result";
private static final int READ_SIZE = 16 * 1024;
/**
* Time between two consecutive partial results are propagated upstream
*
* TODO 5399646: make this configurable
*/
@VisibleForTesting static final long TIME_BETWEEN_PARTIAL_RESULTS_MS = 100;
private final PooledByteBufferFactory mPooledByteBufferFactory;
private final ByteArrayPool mByteArrayPool;
public NetworkFetchProducer(
PooledByteBufferFactory pooledByteBufferFactory,
ByteArrayPool byteArrayPool) {
mPooledByteBufferFactory = pooledByteBufferFactory;
mByteArrayPool = byteArrayPool;
}
/**
* Returns the name of the producer.
*
* <p>This name is passed to the {@link ProducerListener}s.
*/
protected String getProducerName() {
return PRODUCER_NAME;
}
@Override
public void produceResults(
Consumer<CloseableReference<PooledByteBuffer>> consumer,
ProducerContext context) {
context.getListener().onProducerStart(
context.getId(),
getProducerName());
RS requestState = newRequestState(consumer, context);
fetchImage(requestState);
}
/**
* Returns an instance of the {@link NfpRequestState}-derived object used to store state.
*/
protected abstract RS newRequestState(
Consumer<CloseableReference<PooledByteBuffer>> consumer,
ProducerContext context);
/**
* Subclasses should override this method to actually call their network stack.
*
* <p>It is strongly recommended that this method be asynchronous.
*/
protected abstract void fetchImage(final RS requestState);
protected void processResult(
RS requestState,
InputStream responseData,
int responseContentLength,
boolean propagateIntermediateResults)
throws IOException {
final PooledByteBufferOutputStream pooledOutputStream;
if (responseContentLength > 0) {
pooledOutputStream = mPooledByteBufferFactory.newOutputStream(responseContentLength);
} else {
pooledOutputStream = mPooledByteBufferFactory.newOutputStream();
}
final byte[] ioArray = mByteArrayPool.get(READ_SIZE);
try {
int length;
while ((length = responseData.read(ioArray)) >= 0) {
if (length > 0) {
pooledOutputStream.write(ioArray, 0, length);
if (propagateIntermediateResults) {
maybeHandleIntermediateResult(pooledOutputStream, requestState);
}
}
}
handleFinalResult(pooledOutputStream, requestState);
} finally {
mByteArrayPool.release(ioArray);
pooledOutputStream.close();
}
}
private void maybeHandleIntermediateResult(
PooledByteBufferOutputStream pooledOutputStream,
NfpRequestState requestState) {
final long nowMs = SystemClock.elapsedRealtime();
if (nowMs - requestState.getLastIntermediateResultTimeMs() >= TIME_BETWEEN_PARTIAL_RESULTS_MS) {
requestState.setLastIntermediateResultTimeMs(nowMs);
requestState.getListener().onProducerEvent(
requestState.getId(), getProducerName(), INTERMEDIATE_RESULT_PRODUCER_EVENT);
passResultToConsumer(pooledOutputStream, false, requestState.getConsumer());
}
}
protected void handleFinalResult(
PooledByteBufferOutputStream pooledOutputStream,
RS requestState) {
requestState.getListener().onProducerFinishWithSuccess(
requestState.getId(),
getProducerName(),
getExtraMap(pooledOutputStream.size(), requestState));
passResultToConsumer(pooledOutputStream, true, requestState.getConsumer());
}
@VisibleForTesting
void passResultToConsumer(
PooledByteBufferOutputStream pooledOutputStream,
boolean isFinal,
Consumer<CloseableReference<PooledByteBuffer>> consumer) {
CloseableReference<PooledByteBuffer> result =
CloseableReference.of(pooledOutputStream.toByteBuffer());
consumer.onNewResult(result, isFinal);
result.close();
}
private @Nullable Map<String, String> getExtraMap(
int byteSize,
RS requestState) {
if (!requestState.getListener().requiresExtraMap(requestState.getId())) {
return null;
}
return buildExtraMapForFinalResult(byteSize, requestState);
}
/**
* Override this to provide a map containing extra parameters to pass to the listeners.
*
* @return An immutable map of the parameters. Attempts to modify this map afterwards
* will result in an exception being thrown.
*/
protected @Nullable Map<String, String> buildExtraMapForFinalResult(
int byteSize,
RS requestState) {
return null;
}
/**
* Called upon a failure in the network stack.
*
* @param requestState Request-specific data.
* @param e The exception thrown.
* @param extraMap An immutable map of the parameters. Attempts to modify this map afterwards
* will result in an exception being thrown.
*/
protected void onFailure(RS requestState, Throwable e, Map<String, String> extraMap) {
requestState.getListener().onProducerFinishWithFailure(
requestState.getId(),
getProducerName(),
e,
extraMap);
requestState.getConsumer().onFailure(e);
}
/**
* Called upon a cancellation of the request.
*
* @param requestState Request-specific data.
* @param extraMap An immutable map of the parameters. Attempts to modify this map afterwards
* will result in an exception being thrown.
*/
protected void onCancellation(RS requestState, Map<String, String> extraMap) {
requestState.getListener().onProducerFinishWithCancellation(
requestState.getId(),
getProducerName(),
extraMap);
requestState.getConsumer().onCancellation();
}
}