/* * Copyright (C) 2013 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.android.photos.drawables; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.Log; import com.android.photos.data.GalleryBitmapPool; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public abstract class AutoThumbnailDrawable<T> extends Drawable { private static final String TAG = "AutoThumbnailDrawable"; private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor(); private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance(); private static byte[] sTempStorage = new byte[64 * 1024]; // UI thread only private Paint mPaint = new Paint(); private Matrix mDrawMatrix = new Matrix(); // Decoder thread only private BitmapFactory.Options mOptions = new BitmapFactory.Options(); // Shared, guarded by mLock private Object mLock = new Object(); private Bitmap mBitmap; protected T mData; private boolean mIsQueued; private int mImageWidth, mImageHeight; private Rect mBounds = new Rect(); private int mSampleSize = 1; public AutoThumbnailDrawable() { mPaint.setAntiAlias(true); mPaint.setFilterBitmap(true); mDrawMatrix.reset(); mOptions.inTempStorage = sTempStorage; } protected abstract byte[] getPreferredImageBytes(T data); protected abstract InputStream getFallbackImageStream(T data); protected abstract boolean dataChangedLocked(T data); public void setImage(T data, int width, int height) { if (!dataChangedLocked(data)) return; synchronized (mLock) { mImageWidth = width; mImageHeight = height; mData = data; setBitmapLocked(null); refreshSampleSizeLocked(); } invalidateSelf(); } private void setBitmapLocked(Bitmap b) { if (b == mBitmap) { return; } if (mBitmap != null) { sBitmapPool.put(mBitmap); } mBitmap = b; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); synchronized (mLock) { mBounds.set(bounds); if (mBounds.isEmpty()) { mBitmap = null; } else { refreshSampleSizeLocked(); updateDrawMatrixLocked(); } } invalidateSelf(); } @Override public void draw(Canvas canvas) { if (mBitmap != null) { canvas.save(); canvas.clipRect(mBounds); canvas.concat(mDrawMatrix); canvas.drawBitmap(mBitmap, 0, 0, mPaint); canvas.restore(); } else { // TODO: Draw placeholder...? } } private void updateDrawMatrixLocked() { if (mBitmap == null || mBounds.isEmpty()) { mDrawMatrix.reset(); return; } float scale; float dx = 0, dy = 0; int dwidth = mBitmap.getWidth(); int dheight = mBitmap.getHeight(); int vwidth = mBounds.width(); int vheight = mBounds.height(); // Calculates a matrix similar to ScaleType.CENTER_CROP if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; } if (scale < .8f) { Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize); } else if (scale > 1.5f) { Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize); } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); } private int calculateSampleSizeLocked(int dwidth, int dheight) { float scale; int vwidth = mBounds.width(); int vheight = mBounds.height(); // Inverse of updateDrawMatrixLocked if (dwidth * vheight > vwidth * dheight) { scale = (float) dheight / (float) vheight; } else { scale = (float) dwidth / (float) vwidth; } int result = Math.round(scale); return result > 0 ? result : 1; } private void refreshSampleSizeLocked() { if (mBounds.isEmpty() || mImageWidth == 0 || mImageHeight == 0) { return; } int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); if (sampleSize != mSampleSize || mBitmap == null) { mSampleSize = sampleSize; loadBitmapLocked(); } } private void loadBitmapLocked() { if (!mIsQueued && !mBounds.isEmpty()) { unscheduleSelf(mUpdateBitmap); sThreadPool.execute(mLoadBitmap); mIsQueued = true; } } public float getAspectRatio() { return (float) mImageWidth / (float) mImageHeight; } @Override public int getIntrinsicWidth() { return -1; } @Override public int getIntrinsicHeight() { return -1; } @Override public int getOpacity() { Bitmap bm = mBitmap; return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; } @Override public void setAlpha(int alpha) { int oldAlpha = mPaint.getAlpha(); if (alpha != oldAlpha) { mPaint.setAlpha(alpha); invalidateSelf(); } } @Override public void setColorFilter(ColorFilter cf) { mPaint.setColorFilter(cf); invalidateSelf(); } private final Runnable mLoadBitmap = new Runnable() { @Override public void run() { T data; synchronized (mLock) { data = mData; } int preferredSampleSize = 1; byte[] preferred = getPreferredImageBytes(data); boolean hasPreferred = (preferred != null && preferred.length > 0); if (hasPreferred) { mOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); mOptions.inJustDecodeBounds = false; } int sampleSize, width, height; synchronized (mLock) { if (dataChangedLocked(data)) { return; } width = mImageWidth; height = mImageHeight; if (hasPreferred) { preferredSampleSize = calculateSampleSizeLocked( mOptions.outWidth, mOptions.outHeight); } sampleSize = calculateSampleSizeLocked(width, height); mIsQueued = false; } Bitmap b = null; InputStream is = null; try { if (hasPreferred) { mOptions.inSampleSize = preferredSampleSize; mOptions.inBitmap = sBitmapPool.get( mOptions.outWidth / preferredSampleSize, mOptions.outHeight / preferredSampleSize); b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); if (mOptions.inBitmap != null && b != mOptions.inBitmap) { sBitmapPool.put(mOptions.inBitmap); mOptions.inBitmap = null; } } if (b == null) { is = getFallbackImageStream(data); mOptions.inSampleSize = sampleSize; mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize); b = BitmapFactory.decodeStream(is, null, mOptions); if (mOptions.inBitmap != null && b != mOptions.inBitmap) { sBitmapPool.put(mOptions.inBitmap); mOptions.inBitmap = null; } } } catch (Exception e) { Log.d(TAG, "Failed to fetch bitmap", e); return; } finally { try { if (is != null) { is.close(); } } catch (Exception e) {} if (b != null) { synchronized (mLock) { if (!dataChangedLocked(data)) { setBitmapLocked(b); scheduleSelf(mUpdateBitmap, 0); } } } } } }; private final Runnable mUpdateBitmap = new Runnable() { @Override public void run() { synchronized (AutoThumbnailDrawable.this) { updateDrawMatrixLocked(); invalidateSelf(); } } }; }