/* * 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 android.graphics.Bitmap; import com.facebook.common.internal.ImmutableMap; import com.facebook.common.internal.Preconditions; import com.facebook.common.references.CloseableReference; import com.facebook.common.util.UriUtil; import com.facebook.imagepipeline.common.ImageDecodeOptions; import com.facebook.imagepipeline.decoder.ImageDecoder; import com.facebook.imagepipeline.decoder.ProgressiveJpegConfig; import com.facebook.imagepipeline.decoder.ProgressiveJpegParser; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.image.CloseableStaticBitmap; import com.facebook.imagepipeline.image.EncodedImage; import com.facebook.imagepipeline.image.ImmutableQualityInfo; import com.facebook.imagepipeline.image.QualityInfo; import com.facebook.imagepipeline.memory.ByteArrayPool; import com.facebook.imagepipeline.request.ImageRequest; import static com.facebook.imagepipeline.producers.JobScheduler.JobRunnable; /** * 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 BITMAP_SIZE_KEY = "bitmapSize"; 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<EncodedImage> mInputProducer; private final boolean mDownsampleEnabled; private final boolean mDownsampleEnabledForNetwork; public DecodeProducer( final ByteArrayPool byteArrayPool, final Executor executor, final ImageDecoder imageDecoder, final ProgressiveJpegConfig progressiveJpegConfig, final boolean downsampleEnabled, final boolean downsampleEnabledForNetwork, final Producer<EncodedImage> inputProducer) { mByteArrayPool = Preconditions.checkNotNull(byteArrayPool); mExecutor = Preconditions.checkNotNull(executor); mImageDecoder = Preconditions.checkNotNull(imageDecoder); mProgressiveJpegConfig = Preconditions.checkNotNull(progressiveJpegConfig); mDownsampleEnabled = downsampleEnabled; mDownsampleEnabledForNetwork = downsampleEnabledForNetwork; mInputProducer = Preconditions.checkNotNull(inputProducer); } @Override public void produceResults( final Consumer<CloseableReference<CloseableImage>> consumer, final ProducerContext producerContext) { final ImageRequest imageRequest = producerContext.getImageRequest(); ProgressiveDecoder progressiveDecoder; if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) { progressiveDecoder = new LocalImagesProgressiveDecoder(consumer, producerContext); } else { ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool); progressiveDecoder = new NetworkImagesProgressiveDecoder( consumer, producerContext, jpegParser, mProgressiveJpegConfig); } mInputProducer.produceResults(progressiveDecoder, producerContext); } private abstract class ProgressiveDecoder extends DelegatingConsumer< EncodedImage, CloseableReference<CloseableImage>> { private final ProducerContext mProducerContext; private final ProducerListener mProducerListener; private final ImageDecodeOptions mImageDecodeOptions; @GuardedBy("this") private boolean mIsFinished; private final JobScheduler mJobScheduler; public ProgressiveDecoder( final Consumer<CloseableReference<CloseableImage>> consumer, final ProducerContext producerContext) { super(consumer); mProducerContext = producerContext; mProducerListener = producerContext.getListener(); mImageDecodeOptions = producerContext.getImageRequest().getImageDecodeOptions(); mIsFinished = false; JobRunnable job = new JobRunnable() { @Override public void run(EncodedImage encodedImage, boolean isLast) { if (encodedImage != null) { if (mDownsampleEnabled) { ImageRequest request = producerContext.getImageRequest(); if (mDownsampleEnabledForNetwork || !UriUtil.isNetworkUri(request.getSourceUri())) { encodedImage.setSampleSize(DownsampleUtil.determineSampleSize( request, encodedImage)); } } doDecode(encodedImage, isLast); } } }; mJobScheduler = new JobScheduler(mExecutor, job, mImageDecodeOptions.minDecodeIntervalMs); mProducerContext.addCallbacks( new BaseProducerContextCallbacks() { @Override public void onIsIntermediateResultExpectedChanged() { if (mProducerContext.isIntermediateResultExpected()) { mJobScheduler.scheduleJob(); } } }); } @Override public void onNewResultImpl(EncodedImage newResult, boolean isLast) { if (isLast && !EncodedImage.isValid(newResult)) { handleError(new NullPointerException("Encoded image is not valid.")); return; } if (!updateDecodeJob(newResult, isLast)) { return; } if (isLast || mProducerContext.isIntermediateResultExpected()) { mJobScheduler.scheduleJob(); } } @Override public void onFailureImpl(Throwable t) { handleError(t); } @Override public void onCancellationImpl() { handleCancellation(); } /** Updates the decode job. */ protected boolean updateDecodeJob(EncodedImage ref, boolean isLast) { return mJobScheduler.updateJob(ref, isLast); } /** Performs the decode synchronously. */ private void doDecode(EncodedImage encodedImage, boolean isLast) { if (isFinished() || !EncodedImage.isValid(encodedImage)) { return; } try { long queueTime = mJobScheduler.getQueuedTime(); int length = isLast ? encodedImage.getSize() : getIntermediateImageEndOffset(encodedImage); QualityInfo quality = isLast ? ImmutableQualityInfo.FULL_QUALITY : getQualityInfo(); mProducerListener.onProducerStart(mProducerContext.getId(), PRODUCER_NAME); CloseableImage image = null; try { image = mImageDecoder.decodeImage(encodedImage, length, quality, mImageDecodeOptions); } catch (Exception e) { Map<String, String> extraMap = getExtraMap(image, queueTime, quality, isLast); mProducerListener. onProducerFinishWithFailure(mProducerContext.getId(), PRODUCER_NAME, e, extraMap); handleError(e); return; } Map<String, String> extraMap = getExtraMap(image, queueTime, quality, isLast); mProducerListener. onProducerFinishWithSuccess(mProducerContext.getId(), PRODUCER_NAME, extraMap); handleResult(image, isLast); } finally { EncodedImage.closeSafely(encodedImage); } } private Map<String, String> getExtraMap( @Nullable CloseableImage image, long queueTime, QualityInfo quality, boolean isFinal) { if (!mProducerListener.requiresExtraMap(mProducerContext.getId())) { return null; } String queueStr = String.valueOf(queueTime); String qualityStr = String.valueOf(quality.isOfGoodEnoughQuality()); String finalStr = String.valueOf(isFinal); if (image instanceof CloseableStaticBitmap) { Bitmap bitmap = ((CloseableStaticBitmap) image).getUnderlyingBitmap(); String sizeStr = bitmap.getWidth() + "x" + bitmap.getHeight(); return ImmutableMap.of( BITMAP_SIZE_KEY, sizeStr, JobScheduler.QUEUE_TIME_KEY, queueStr, HAS_GOOD_QUALITY_KEY, qualityStr, IS_FINAL_KEY, finalStr); } else { return ImmutableMap.of( JobScheduler.QUEUE_TIME_KEY, queueStr, HAS_GOOD_QUALITY_KEY, qualityStr, IS_FINAL_KEY, finalStr); } } /** * @return true if producer is finished */ private synchronized boolean isFinished() { return mIsFinished; } /** * Finishes if not already finished and <code>shouldFinish</code> is specified. * <p> If just finished, the intermediate image gets released. */ private void maybeFinish(boolean shouldFinish) { synchronized (ProgressiveDecoder.this) { if (!shouldFinish || mIsFinished) { return; } mIsFinished = true; } mJobScheduler.clearJob(); } /** * 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(); } protected abstract int getIntermediateImageEndOffset(EncodedImage encodedImage); protected abstract QualityInfo getQualityInfo(); } private class LocalImagesProgressiveDecoder extends ProgressiveDecoder { public LocalImagesProgressiveDecoder( final Consumer<CloseableReference<CloseableImage>> consumer, final ProducerContext producerContext) { super(consumer, producerContext); } @Override protected synchronized boolean updateDecodeJob(EncodedImage encodedImage, boolean isLast) { if (!isLast) { return false; } return super.updateDecodeJob(encodedImage, isLast); } @Override protected int getIntermediateImageEndOffset(EncodedImage encodedImage) { return encodedImage.getSize(); } @Override protected QualityInfo getQualityInfo() { return ImmutableQualityInfo.of(0, false, false); } } private 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(EncodedImage encodedImage, boolean isLast) { boolean ret = super.updateDecodeJob(encodedImage, isLast); if (!isLast && EncodedImage.isValid(encodedImage)) { if (!mProgressiveJpegParser.parseMoreData(encodedImage)) { return false; } int scanNum = mProgressiveJpegParser.getBestScanNumber(); if (scanNum <= mLastScheduledScanNumber || scanNum < mProgressiveJpegConfig.getNextScanNumberToDecode( mLastScheduledScanNumber)) { return false; } mLastScheduledScanNumber = scanNum; } return ret; } @Override protected int getIntermediateImageEndOffset(EncodedImage encodedImage) { return mProgressiveJpegParser.getBestScanEndOffset(); } @Override protected QualityInfo getQualityInfo() { return mProgressiveJpegConfig.getQualityInfo(mProgressiveJpegParser.getBestScanNumber()); } } }