/*
* 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 javax.annotation.concurrent.GuardedBy;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import android.os.SystemClock;
import com.facebook.common.executors.UiThreadExecutorService;
import com.facebook.common.internal.ImmutableMap;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.util.UriUtil;
import com.facebook.imagepipeline.common.ImageDecodeOptions;
import com.facebook.imagepipeline.decoder.ProgressiveJpegConfig;
import com.facebook.imagepipeline.decoder.ImageDecoder;
import com.facebook.imagepipeline.decoder.ProgressiveJpegParser;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.ImmutableQualityInfo;
import com.facebook.imagepipeline.image.QualityInfo;
import com.facebook.imagepipeline.memory.ByteArrayPool;
import com.facebook.imagepipeline.memory.PooledByteBuffer;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imageformat.ImageFormat;
/**
* Decodes images.
*
* <p/> Progressive JPEGs are decoded progressively as new data arrives.
*/
public class DecodeProducer implements Producer<CloseableReference<CloseableImage>> {
public static final String PRODUCER_NAME = "DecodeProducer";
// keys for extra map
private static final String QUEUE_TIME_KEY = "queueTime";
private static final String HAS_GOOD_QUALITY_KEY = "hasGoodQuality";
private static final String IS_FINAL_KEY = "isFinal";
private final ByteArrayPool mByteArrayPool;
private final Executor mExecutor;
private final ImageDecoder mImageDecoder;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private final Producer<CloseableReference<PooledByteBuffer>> mNextProducer;
public DecodeProducer(
final ByteArrayPool byteArrayPool,
final Executor executor,
final ImageDecoder imageDecoder,
final ProgressiveJpegConfig progressiveJpegConfig,
final Producer<CloseableReference<PooledByteBuffer>> nextProducer) {
mByteArrayPool = Preconditions.checkNotNull(byteArrayPool);
mExecutor = Preconditions.checkNotNull(executor);
mImageDecoder = Preconditions.checkNotNull(imageDecoder);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mNextProducer = Preconditions.checkNotNull(nextProducer);
}
@Override
public void produceResults(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext context) {
final ImageRequest imageRequest = context.getImageRequest();
ProgressiveDecoder progressiveDecoder;
if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) {
progressiveDecoder = new LocalImagesProgressiveDecoder(consumer, context);
} else {
ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool);
progressiveDecoder = new NetworkImagesProgressiveDecoder(
consumer,
context,
jpegParser,
mProgressiveJpegConfig);
}
mNextProducer.produceResults(progressiveDecoder, context);
}
@VisibleForTesting
abstract class ProgressiveDecoder extends DelegatingConsumer<
CloseableReference<PooledByteBuffer>,
CloseableReference<CloseableImage>> {
protected final ProducerContext mProducerContext;
private final ProducerListener mProducerListener;
private final ImageDecodeOptions mImageDecodeOptions;
private final Runnable mSubmitDecodeRunnable;
@GuardedBy("this")
private boolean mIsFinished;
// This class is responsible for closing old, non-null reference, and for storing the reference
// to the latest data. One thing to note is that the reference is overtaken in doDecode().
// Right before decode happens, reference is cloned (to be held during the decode), and then
// released (so that we don't issue another decode of the same image). The cloned reference gets
// released after decode finishes. As a slight optimization, instead of cloning and releasing,
// reference is just moved.
@GuardedBy("this")
@VisibleForTesting CloseableReference<PooledByteBuffer> mImageBytesRef;
@GuardedBy("this")
private boolean mIsLast;
@GuardedBy("this")
private boolean mIsDecodeSubmitted;
@GuardedBy("this")
private long mLastDecodeTime;
public ProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext) {
super(consumer);
mProducerContext = producerContext;
mProducerListener = producerContext.getListener();
mImageDecodeOptions = producerContext.getImageRequest().getImageDecodeOptions();
mIsFinished = false;
mProducerContext.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onIsIntermediateResultExpectedChanged() {
if (mProducerContext.isIntermediateResultExpected()) {
scheduleDecodeJob(mImageDecodeOptions.minDecodeIntervalMs);
}
}
});
mSubmitDecodeRunnable = new Runnable() {
@Override
public void run() {
submitDecode();
}
};
}
@Override
public void onNewResultImpl(CloseableReference<PooledByteBuffer> newResult, boolean isLast) {
if (!updateDecodeJob(newResult, isLast)) {
return;
}
if (isLast || mProducerContext.isIntermediateResultExpected()) {
scheduleDecodeJob(isLast ? 0 : mImageDecodeOptions.minDecodeIntervalMs);
}
}
@Override
public void onFailureImpl(Throwable t) {
handleError(t);
}
@Override
public void onCancellationImpl() {
handleCancellation();
}
/** Updates the decode job. */
protected synchronized boolean updateDecodeJob(
CloseableReference<PooledByteBuffer> imageBytesRef,
boolean isLast) {
// ignore invalid intermediate results (should not happen ever, but being defensive)
if (!isLast && !CloseableReference.isValid(imageBytesRef)) {
return false;
}
CloseableReference.closeSafely(mImageBytesRef);
mImageBytesRef = CloseableReference.cloneOrNull(imageBytesRef);
mIsLast = isLast;
return true;
}
/** Schedules the decode, but no sooner than minDecodeIntervalMs since the last decode. */
private synchronized void scheduleDecodeJob(int minDecodeIntervalMs) {
if (!mIsDecodeSubmitted) {
mIsDecodeSubmitted = true;
long now = SystemClock.uptimeMillis();
long when = Math.max(mLastDecodeTime + minDecodeIntervalMs, now);
if (when > now) {
UiThreadExecutorService.getInstance()
.schedule(mSubmitDecodeRunnable, when - now, TimeUnit.MILLISECONDS);
} else {
mSubmitDecodeRunnable.run();
}
}
}
/** Submits the decode to the executor. */
protected void submitDecode() {
final long submitTime = SystemClock.uptimeMillis();
mExecutor.execute(
new Runnable() {
@Override
public void run() {
final long queueTime = SystemClock.uptimeMillis() - submitTime;
doDecode(queueTime);
}
});
}
/** Performs the decode synchronously. */
private void doDecode(long queueTime) {
CloseableReference<PooledByteBuffer> bytesRef;
boolean isLast;
synchronized (ProgressiveDecoder.this) {
bytesRef = mImageBytesRef;
mImageBytesRef = null;
isLast = mIsLast;
mIsDecodeSubmitted = false;
mLastDecodeTime = SystemClock.uptimeMillis();
}
try {
if (isFinished() || !CloseableReference.isValid(bytesRef)) {
return;
}
ImageFormat format = isLast ? ImageFormat.UNKNOWN : getImageFormat(bytesRef);
int length = isLast ? bytesRef.get().size() : getIntermediateImageEndOffset(bytesRef);
QualityInfo quality = isLast ? ImmutableQualityInfo.FULL_QUALITY : getQualityInfo(bytesRef);
mProducerListener.onProducerStart(mProducerContext.getId(), PRODUCER_NAME);
CloseableImage decodedImage;
try {
decodedImage =
mImageDecoder.decodeImage(bytesRef, format, length, quality, mImageDecodeOptions);
} catch (Exception e) {
Map<String, String> extraMap = getExtraMap(queueTime, quality, isLast);
mProducerListener.
onProducerFinishWithFailure(mProducerContext.getId(), PRODUCER_NAME, e, extraMap);
handleError(e);
return;
}
Map<String, String> extraMap = getExtraMap(queueTime, quality, isLast);
mProducerListener.
onProducerFinishWithSuccess(mProducerContext.getId(), PRODUCER_NAME, extraMap);
handleResult(decodedImage, isLast);
} finally {
CloseableReference.closeSafely(bytesRef);
}
}
private Map<String, String> getExtraMap(
final long queueTime,
final QualityInfo qualityInfo,
final boolean isFinal) {
if (!mProducerListener.requiresExtraMap(mProducerContext.getId())) {
return null;
}
return ImmutableMap.of(
QUEUE_TIME_KEY,
String.valueOf(queueTime),
HAS_GOOD_QUALITY_KEY,
String.valueOf(qualityInfo.isOfGoodEnoughQuality()),
IS_FINAL_KEY,
String.valueOf(isFinal));
}
/**
* @return true if producer is finished
*/
private synchronized boolean isFinished() {
return mIsFinished;
}
/**
* Finishes if not already finished and {@code finish} is specified.
* <p> If just finished, the intermediate image gets released.
*/
private synchronized void maybeFinish(boolean finish) {
if (mIsFinished) {
return;
}
mIsFinished = finish;
if (finish) {
CloseableReference.closeSafely(mImageBytesRef);
mImageBytesRef = null;
}
}
/**
* Notifies consumer of new result and finishes if the result is final.
*/
private void handleResult(final CloseableImage decodedImage, final boolean isFinal) {
CloseableReference<CloseableImage> decodedImageRef = CloseableReference.of(decodedImage);
try {
maybeFinish(isFinal);
getConsumer().onNewResult(decodedImageRef, isFinal);
} finally {
CloseableReference.closeSafely(decodedImageRef);
}
}
/**
* Notifies consumer about the failure and finishes.
*/
private void handleError(Throwable t) {
maybeFinish(true);
getConsumer().onFailure(t);
}
/**
* Notifies consumer about the cancellation and finishes.
*/
private void handleCancellation() {
maybeFinish(true);
getConsumer().onCancellation();
}
/**
* All these abstract methods are thread-safe.
*/
@Nullable protected abstract ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef);
protected abstract int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef);
protected abstract QualityInfo getQualityInfo(
CloseableReference<PooledByteBuffer> imageBytesRef);
}
class LocalImagesProgressiveDecoder extends ProgressiveDecoder {
public LocalImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext) {
super(consumer, producerContext);
}
@Override
@Nullable protected ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return null;
}
@Override
protected int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return imageBytesRef.get().size();
}
@Override
protected QualityInfo getQualityInfo(CloseableReference<PooledByteBuffer> imageBytesRef) {
return ImmutableQualityInfo.of(0, false, false);
}
}
class NetworkImagesProgressiveDecoder extends ProgressiveDecoder {
private final ProgressiveJpegParser mProgressiveJpegParser;
private final ProgressiveJpegConfig mProgressiveJpegConfig;
private int mLastScheduledScanNumber;
public NetworkImagesProgressiveDecoder(
final Consumer<CloseableReference<CloseableImage>> consumer,
final ProducerContext producerContext,
final ProgressiveJpegParser progressiveJpegParser,
final ProgressiveJpegConfig progressiveJpegConfig) {
super(consumer, producerContext);
mProgressiveJpegParser = Preconditions.checkNotNull(progressiveJpegParser);
mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig);
mLastScheduledScanNumber = 0;
}
@Override
protected synchronized boolean updateDecodeJob(
CloseableReference<PooledByteBuffer> imageBytesRef,
boolean isLast) {
boolean ret = super.updateDecodeJob(imageBytesRef, isLast);
if (!isLast && CloseableReference.isValid(imageBytesRef)) {
if (!mProgressiveJpegParser.parseMoreData(imageBytesRef)) {
return false;
}
int scanNum = mProgressiveJpegParser.getBestScanNumber();
if (scanNum <= mLastScheduledScanNumber ||
scanNum < mProgressiveJpegConfig.getNextScanNumberToDecode(mLastScheduledScanNumber)) {
return false;
}
mLastScheduledScanNumber = scanNum;
}
return ret;
}
@Override
@Nullable protected ImageFormat getImageFormat(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegParser.isJpeg() ? ImageFormat.JPEG : ImageFormat.UNKNOWN;
}
@Override
protected int getIntermediateImageEndOffset(
CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegParser.getBestScanEndOffset();
}
@Override
protected QualityInfo getQualityInfo(CloseableReference<PooledByteBuffer> imageBytesRef) {
return mProgressiveJpegConfig.getQualityInfo(mProgressiveJpegParser.getBestScanNumber());
}
}
}