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