1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.photos.drawables; 18 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapFactory; 21 import android.graphics.Canvas; 22 import android.graphics.ColorFilter; 23 import android.graphics.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.PixelFormat; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.util.Log; 29 30 import com.android.photos.data.GalleryBitmapPool; 31 32 import java.io.InputStream; 33 import java.util.concurrent.ExecutorService; 34 import java.util.concurrent.Executors; 35 36 public abstract class AutoThumbnailDrawable<T> extends Drawable { 37 38 private static final String TAG = "AutoThumbnailDrawable"; 39 40 private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor(); 41 private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance(); 42 private static byte[] sTempStorage = new byte[64 * 1024]; 43 44 // UI thread only 45 private Paint mPaint = new Paint(); 46 private Matrix mDrawMatrix = new Matrix(); 47 48 // Decoder thread only 49 private BitmapFactory.Options mOptions = new BitmapFactory.Options(); 50 51 // Shared, guarded by mLock 52 private Object mLock = new Object(); 53 private Bitmap mBitmap; 54 protected T mData; 55 private boolean mIsQueued; 56 private int mImageWidth, mImageHeight; 57 private Rect mBounds = new Rect(); 58 private int mSampleSize = 1; 59 AutoThumbnailDrawable()60 public AutoThumbnailDrawable() { 61 mPaint.setAntiAlias(true); 62 mPaint.setFilterBitmap(true); 63 mDrawMatrix.reset(); 64 mOptions.inTempStorage = sTempStorage; 65 } 66 getPreferredImageBytes(T data)67 protected abstract byte[] getPreferredImageBytes(T data); getFallbackImageStream(T data)68 protected abstract InputStream getFallbackImageStream(T data); dataChangedLocked(T data)69 protected abstract boolean dataChangedLocked(T data); 70 setImage(T data, int width, int height)71 public void setImage(T data, int width, int height) { 72 if (!dataChangedLocked(data)) return; 73 synchronized (mLock) { 74 mImageWidth = width; 75 mImageHeight = height; 76 mData = data; 77 setBitmapLocked(null); 78 refreshSampleSizeLocked(); 79 } 80 invalidateSelf(); 81 } 82 setBitmapLocked(Bitmap b)83 private void setBitmapLocked(Bitmap b) { 84 if (b == mBitmap) { 85 return; 86 } 87 if (mBitmap != null) { 88 sBitmapPool.put(mBitmap); 89 } 90 mBitmap = b; 91 } 92 93 @Override onBoundsChange(Rect bounds)94 protected void onBoundsChange(Rect bounds) { 95 super.onBoundsChange(bounds); 96 synchronized (mLock) { 97 mBounds.set(bounds); 98 if (mBounds.isEmpty()) { 99 mBitmap = null; 100 } else { 101 refreshSampleSizeLocked(); 102 updateDrawMatrixLocked(); 103 } 104 } 105 invalidateSelf(); 106 } 107 108 @Override draw(Canvas canvas)109 public void draw(Canvas canvas) { 110 if (mBitmap != null) { 111 canvas.save(); 112 canvas.clipRect(mBounds); 113 canvas.concat(mDrawMatrix); 114 canvas.drawBitmap(mBitmap, 0, 0, mPaint); 115 canvas.restore(); 116 } else { 117 // TODO: Draw placeholder...? 118 } 119 } 120 updateDrawMatrixLocked()121 private void updateDrawMatrixLocked() { 122 if (mBitmap == null || mBounds.isEmpty()) { 123 mDrawMatrix.reset(); 124 return; 125 } 126 127 float scale; 128 float dx = 0, dy = 0; 129 130 int dwidth = mBitmap.getWidth(); 131 int dheight = mBitmap.getHeight(); 132 int vwidth = mBounds.width(); 133 int vheight = mBounds.height(); 134 135 // Calculates a matrix similar to ScaleType.CENTER_CROP 136 if (dwidth * vheight > vwidth * dheight) { 137 scale = (float) vheight / (float) dheight; 138 dx = (vwidth - dwidth * scale) * 0.5f; 139 } else { 140 scale = (float) vwidth / (float) dwidth; 141 dy = (vheight - dheight * scale) * 0.5f; 142 } 143 if (scale < .8f) { 144 Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize); 145 } else if (scale > 1.5f) { 146 Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize); 147 } 148 149 mDrawMatrix.setScale(scale, scale); 150 mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); 151 } 152 calculateSampleSizeLocked(int dwidth, int dheight)153 private int calculateSampleSizeLocked(int dwidth, int dheight) { 154 float scale; 155 156 int vwidth = mBounds.width(); 157 int vheight = mBounds.height(); 158 159 // Inverse of updateDrawMatrixLocked 160 if (dwidth * vheight > vwidth * dheight) { 161 scale = (float) dheight / (float) vheight; 162 } else { 163 scale = (float) dwidth / (float) vwidth; 164 } 165 int result = Math.round(scale); 166 return result > 0 ? result : 1; 167 } 168 refreshSampleSizeLocked()169 private void refreshSampleSizeLocked() { 170 if (mBounds.isEmpty() || mImageWidth == 0 || mImageHeight == 0) { 171 return; 172 } 173 174 int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); 175 if (sampleSize != mSampleSize || mBitmap == null) { 176 mSampleSize = sampleSize; 177 loadBitmapLocked(); 178 } 179 } 180 loadBitmapLocked()181 private void loadBitmapLocked() { 182 if (!mIsQueued && !mBounds.isEmpty()) { 183 unscheduleSelf(mUpdateBitmap); 184 sThreadPool.execute(mLoadBitmap); 185 mIsQueued = true; 186 } 187 } 188 getAspectRatio()189 public float getAspectRatio() { 190 return (float) mImageWidth / (float) mImageHeight; 191 } 192 193 @Override getIntrinsicWidth()194 public int getIntrinsicWidth() { 195 return -1; 196 } 197 198 @Override getIntrinsicHeight()199 public int getIntrinsicHeight() { 200 return -1; 201 } 202 203 @Override getOpacity()204 public int getOpacity() { 205 Bitmap bm = mBitmap; 206 return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? 207 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 208 } 209 210 @Override setAlpha(int alpha)211 public void setAlpha(int alpha) { 212 int oldAlpha = mPaint.getAlpha(); 213 if (alpha != oldAlpha) { 214 mPaint.setAlpha(alpha); 215 invalidateSelf(); 216 } 217 } 218 219 @Override setColorFilter(ColorFilter cf)220 public void setColorFilter(ColorFilter cf) { 221 mPaint.setColorFilter(cf); 222 invalidateSelf(); 223 } 224 225 private final Runnable mLoadBitmap = new Runnable() { 226 @Override 227 public void run() { 228 T data; 229 synchronized (mLock) { 230 data = mData; 231 } 232 int preferredSampleSize = 1; 233 byte[] preferred = getPreferredImageBytes(data); 234 boolean hasPreferred = (preferred != null && preferred.length > 0); 235 if (hasPreferred) { 236 mOptions.inJustDecodeBounds = true; 237 BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); 238 mOptions.inJustDecodeBounds = false; 239 } 240 int sampleSize, width, height; 241 synchronized (mLock) { 242 if (dataChangedLocked(data)) { 243 return; 244 } 245 width = mImageWidth; 246 height = mImageHeight; 247 if (hasPreferred) { 248 preferredSampleSize = calculateSampleSizeLocked( 249 mOptions.outWidth, mOptions.outHeight); 250 } 251 sampleSize = calculateSampleSizeLocked(width, height); 252 mIsQueued = false; 253 } 254 Bitmap b = null; 255 InputStream is = null; 256 try { 257 if (hasPreferred) { 258 mOptions.inSampleSize = preferredSampleSize; 259 mOptions.inBitmap = sBitmapPool.get( 260 mOptions.outWidth / preferredSampleSize, 261 mOptions.outHeight / preferredSampleSize); 262 b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions); 263 if (mOptions.inBitmap != null && b != mOptions.inBitmap) { 264 sBitmapPool.put(mOptions.inBitmap); 265 mOptions.inBitmap = null; 266 } 267 } 268 if (b == null) { 269 is = getFallbackImageStream(data); 270 mOptions.inSampleSize = sampleSize; 271 mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize); 272 b = BitmapFactory.decodeStream(is, null, mOptions); 273 if (mOptions.inBitmap != null && b != mOptions.inBitmap) { 274 sBitmapPool.put(mOptions.inBitmap); 275 mOptions.inBitmap = null; 276 } 277 } 278 } catch (Exception e) { 279 Log.d(TAG, "Failed to fetch bitmap", e); 280 return; 281 } finally { 282 try { 283 if (is != null) { 284 is.close(); 285 } 286 } catch (Exception e) {} 287 if (b != null) { 288 synchronized (mLock) { 289 if (!dataChangedLocked(data)) { 290 setBitmapLocked(b); 291 scheduleSelf(mUpdateBitmap, 0); 292 } 293 } 294 } 295 } 296 } 297 }; 298 299 private final Runnable mUpdateBitmap = new Runnable() { 300 @Override 301 public void run() { 302 synchronized (AutoThumbnailDrawable.this) { 303 updateDrawMatrixLocked(); 304 invalidateSelf(); 305 } 306 } 307 }; 308 309 } 310