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
17package com.android.photos.drawables;
18
19import android.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.graphics.Canvas;
22import android.graphics.ColorFilter;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.graphics.PixelFormat;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.util.Log;
29
30import com.android.photos.data.GalleryBitmapPool;
31
32import java.io.InputStream;
33import java.util.concurrent.ExecutorService;
34import java.util.concurrent.Executors;
35
36public 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
60    public AutoThumbnailDrawable() {
61        mPaint.setAntiAlias(true);
62        mPaint.setFilterBitmap(true);
63        mDrawMatrix.reset();
64        mOptions.inTempStorage = sTempStorage;
65    }
66
67    protected abstract byte[] getPreferredImageBytes(T data);
68    protected abstract InputStream getFallbackImageStream(T data);
69    protected abstract boolean dataChangedLocked(T data);
70
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
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
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
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
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
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
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
181    private void loadBitmapLocked() {
182        if (!mIsQueued && !mBounds.isEmpty()) {
183            unscheduleSelf(mUpdateBitmap);
184            sThreadPool.execute(mLoadBitmap);
185            mIsQueued = true;
186        }
187    }
188
189    public float getAspectRatio() {
190        return (float) mImageWidth / (float) mImageHeight;
191    }
192
193    @Override
194    public int getIntrinsicWidth() {
195        return -1;
196    }
197
198    @Override
199    public int getIntrinsicHeight() {
200        return -1;
201    }
202
203    @Override
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
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
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