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;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.Bitmap.Config;
24import android.graphics.BitmapFactory;
25import android.graphics.BitmapRegionDecoder;
26import android.graphics.Canvas;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.PorterDuff;
30import android.graphics.Rect;
31import android.net.Uri;
32import android.os.Build;
33import android.os.Build.VERSION_CODES;
34import android.util.Log;
35
36import com.android.gallery3d.common.BitmapUtils;
37import com.android.gallery3d.common.Utils;
38import com.android.gallery3d.exif.ExifInterface;
39import com.android.gallery3d.glrenderer.BasicTexture;
40import com.android.gallery3d.glrenderer.BitmapTexture;
41import com.android.photos.views.TiledImageRenderer;
42
43import java.io.BufferedInputStream;
44import java.io.FileNotFoundException;
45import java.io.IOException;
46import java.io.InputStream;
47
48interface SimpleBitmapRegionDecoder {
49    int getWidth();
50    int getHeight();
51    Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
52}
53
54class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
55    BitmapRegionDecoder mDecoder;
56    private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
57        mDecoder = decoder;
58    }
59    public static SimpleBitmapRegionDecoderWrapper newInstance(
60            String pathName, boolean isShareable) {
61        try {
62            BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
63            if (d != null) {
64                return new SimpleBitmapRegionDecoderWrapper(d);
65            }
66        } catch (IOException e) {
67            Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
68            return null;
69        }
70        return null;
71    }
72    public static SimpleBitmapRegionDecoderWrapper newInstance(
73            InputStream is, boolean isShareable) {
74        try {
75            BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
76            if (d != null) {
77                return new SimpleBitmapRegionDecoderWrapper(d);
78            }
79        } catch (IOException e) {
80            Log.w("BitmapRegionTileSource", "getting decoder failed", e);
81            return null;
82        }
83        return null;
84    }
85    public int getWidth() {
86        return mDecoder.getWidth();
87    }
88    public int getHeight() {
89        return mDecoder.getHeight();
90    }
91    public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
92        return mDecoder.decodeRegion(wantRegion, options);
93    }
94}
95
96class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
97    Bitmap mBuffer;
98    Canvas mTempCanvas;
99    Paint mTempPaint;
100    private DumbBitmapRegionDecoder(Bitmap b) {
101        mBuffer = b;
102    }
103    public static DumbBitmapRegionDecoder newInstance(String pathName) {
104        Bitmap b = BitmapFactory.decodeFile(pathName);
105        if (b != null) {
106            return new DumbBitmapRegionDecoder(b);
107        }
108        return null;
109    }
110    public static DumbBitmapRegionDecoder newInstance(InputStream is) {
111        Bitmap b = BitmapFactory.decodeStream(is);
112        if (b != null) {
113            return new DumbBitmapRegionDecoder(b);
114        }
115        return null;
116    }
117    public int getWidth() {
118        return mBuffer.getWidth();
119    }
120    public int getHeight() {
121        return mBuffer.getHeight();
122    }
123    public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
124        if (mTempCanvas == null) {
125            mTempCanvas = new Canvas();
126            mTempPaint = new Paint();
127            mTempPaint.setFilterBitmap(true);
128        }
129        int sampleSize = Math.max(options.inSampleSize, 1);
130        Bitmap newBitmap = Bitmap.createBitmap(
131                wantRegion.width() / sampleSize,
132                wantRegion.height() / sampleSize,
133                Bitmap.Config.ARGB_8888);
134        mTempCanvas.setBitmap(newBitmap);
135        mTempCanvas.save();
136        mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
137        mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
138        mTempCanvas.restore();
139        mTempCanvas.setBitmap(null);
140        return newBitmap;
141    }
142}
143
144/**
145 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
146 * {@link BitmapRegionDecoder} to wrap a local file
147 */
148@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
149public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
150
151    private static final String TAG = "BitmapRegionTileSource";
152
153    private static final boolean REUSE_BITMAP =
154            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
155    private static final int GL_SIZE_LIMIT = 2048;
156    // This must be no larger than half the size of the GL_SIZE_LIMIT
157    // due to decodePreview being allowed to be up to 2x the size of the target
158    public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
159
160    public static abstract class BitmapSource {
161        private SimpleBitmapRegionDecoder mDecoder;
162        private Bitmap mPreview;
163        private int mPreviewSize;
164        private int mRotation;
165        public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
166        private State mState = State.NOT_LOADED;
167        public BitmapSource(int previewSize) {
168            mPreviewSize = previewSize;
169        }
170        public boolean loadInBackground() {
171            ExifInterface ei = new ExifInterface();
172            if (readExif(ei)) {
173                Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
174                if (ori != null) {
175                    mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
176                }
177            }
178            mDecoder = loadBitmapRegionDecoder();
179            if (mDecoder == null) {
180                mState = State.ERROR_LOADING;
181                return false;
182            } else {
183                int width = mDecoder.getWidth();
184                int height = mDecoder.getHeight();
185                if (mPreviewSize != 0) {
186                    int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
187                    BitmapFactory.Options opts = new BitmapFactory.Options();
188                    opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
189                    opts.inPreferQualityOverSpeed = true;
190
191                    float scale = (float) previewSize / Math.max(width, height);
192                    opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
193                    opts.inJustDecodeBounds = false;
194                    mPreview = loadPreviewBitmap(opts);
195                }
196                mState = State.LOADED;
197                return true;
198            }
199        }
200
201        public State getLoadingState() {
202            return mState;
203        }
204
205        public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
206            return mDecoder;
207        }
208
209        public Bitmap getPreviewBitmap() {
210            return mPreview;
211        }
212
213        public int getPreviewSize() {
214            return mPreviewSize;
215        }
216
217        public int getRotation() {
218            return mRotation;
219        }
220
221        public abstract boolean readExif(ExifInterface ei);
222        public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
223        public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
224    }
225
226    public static class FilePathBitmapSource extends BitmapSource {
227        private String mPath;
228        public FilePathBitmapSource(String path, int previewSize) {
229            super(previewSize);
230            mPath = path;
231        }
232        @Override
233        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
234            SimpleBitmapRegionDecoder d;
235            d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
236            if (d == null) {
237                d = DumbBitmapRegionDecoder.newInstance(mPath);
238            }
239            return d;
240        }
241        @Override
242        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
243            return BitmapFactory.decodeFile(mPath, options);
244        }
245        @Override
246        public boolean readExif(ExifInterface ei) {
247            try {
248                ei.readExif(mPath);
249                return true;
250            } catch (IOException e) {
251                Log.w("BitmapRegionTileSource", "getting decoder failed", e);
252                return false;
253            }
254        }
255    }
256
257    public static class UriBitmapSource extends BitmapSource {
258        private Context mContext;
259        private Uri mUri;
260        public UriBitmapSource(Context context, Uri uri, int previewSize) {
261            super(previewSize);
262            mContext = context;
263            mUri = uri;
264        }
265        private InputStream regenerateInputStream() throws FileNotFoundException {
266            InputStream is = mContext.getContentResolver().openInputStream(mUri);
267            return new BufferedInputStream(is);
268        }
269        @Override
270        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
271            try {
272                InputStream is = regenerateInputStream();
273                SimpleBitmapRegionDecoder regionDecoder =
274                        SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
275                Utils.closeSilently(is);
276                if (regionDecoder == null) {
277                    is = regenerateInputStream();
278                    regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
279                    Utils.closeSilently(is);
280                }
281                return regionDecoder;
282            } catch (FileNotFoundException e) {
283                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
284                return null;
285            } catch (IOException e) {
286                Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e);
287                return null;
288            }
289        }
290        @Override
291        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
292            try {
293                InputStream is = regenerateInputStream();
294                Bitmap b = BitmapFactory.decodeStream(is, null, options);
295                Utils.closeSilently(is);
296                return b;
297            } catch (FileNotFoundException e) {
298                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
299                return null;
300            }
301        }
302        @Override
303        public boolean readExif(ExifInterface ei) {
304            InputStream is = null;
305            try {
306                is = regenerateInputStream();
307                ei.readExif(is);
308                Utils.closeSilently(is);
309                return true;
310            } catch (FileNotFoundException e) {
311                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
312                return false;
313            } catch (IOException e) {
314                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
315                return false;
316            } finally {
317                Utils.closeSilently(is);
318            }
319        }
320    }
321
322    public static class ResourceBitmapSource extends BitmapSource {
323        private Resources mRes;
324        private int mResId;
325        public ResourceBitmapSource(Resources res, int resId, int previewSize) {
326            super(previewSize);
327            mRes = res;
328            mResId = resId;
329        }
330        private InputStream regenerateInputStream() {
331            InputStream is = mRes.openRawResource(mResId);
332            return new BufferedInputStream(is);
333        }
334        @Override
335        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
336            InputStream is = regenerateInputStream();
337            SimpleBitmapRegionDecoder regionDecoder =
338                    SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
339            Utils.closeSilently(is);
340            if (regionDecoder == null) {
341                is = regenerateInputStream();
342                regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
343                Utils.closeSilently(is);
344            }
345            return regionDecoder;
346        }
347        @Override
348        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
349            return BitmapFactory.decodeResource(mRes, mResId, options);
350        }
351        @Override
352        public boolean readExif(ExifInterface ei) {
353            try {
354                InputStream is = regenerateInputStream();
355                ei.readExif(is);
356                Utils.closeSilently(is);
357                return true;
358            } catch (IOException e) {
359                Log.e("BitmapRegionTileSource", "Error reading resource", e);
360                return false;
361            }
362        }
363    }
364
365    SimpleBitmapRegionDecoder mDecoder;
366    int mWidth;
367    int mHeight;
368    int mTileSize;
369    private BasicTexture mPreview;
370    private final int mRotation;
371
372    // For use only by getTile
373    private Rect mWantRegion = new Rect();
374    private Rect mOverlapRegion = new Rect();
375    private BitmapFactory.Options mOptions;
376    private Canvas mCanvas;
377
378    public BitmapRegionTileSource(Context context, BitmapSource source) {
379        mTileSize = TiledImageRenderer.suggestedTileSize(context);
380        mRotation = source.getRotation();
381        mDecoder = source.getBitmapRegionDecoder();
382        if (mDecoder != null) {
383            mWidth = mDecoder.getWidth();
384            mHeight = mDecoder.getHeight();
385            mOptions = new BitmapFactory.Options();
386            mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
387            mOptions.inPreferQualityOverSpeed = true;
388            mOptions.inTempStorage = new byte[16 * 1024];
389            int previewSize = source.getPreviewSize();
390            if (previewSize != 0) {
391                previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
392                // Although this is the same size as the Bitmap that is likely already
393                // loaded, the lifecycle is different and interactions are on a different
394                // thread. Thus to simplify, this source will decode its own bitmap.
395                Bitmap preview = decodePreview(source, previewSize);
396                if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
397                    mPreview = new BitmapTexture(preview);
398                } else {
399                    Log.w(TAG, String.format(
400                            "Failed to create preview of apropriate size! "
401                            + " in: %dx%d, out: %dx%d",
402                            mWidth, mHeight,
403                            preview.getWidth(), preview.getHeight()));
404                }
405            }
406        }
407    }
408
409    @Override
410    public int getTileSize() {
411        return mTileSize;
412    }
413
414    @Override
415    public int getImageWidth() {
416        return mWidth;
417    }
418
419    @Override
420    public int getImageHeight() {
421        return mHeight;
422    }
423
424    @Override
425    public BasicTexture getPreview() {
426        return mPreview;
427    }
428
429    @Override
430    public int getRotation() {
431        return mRotation;
432    }
433
434    @Override
435    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
436        int tileSize = getTileSize();
437        if (!REUSE_BITMAP) {
438            return getTileWithoutReusingBitmap(level, x, y, tileSize);
439        }
440
441        int t = tileSize << level;
442        mWantRegion.set(x, y, x + t, y + t);
443
444        if (bitmap == null) {
445            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
446        }
447
448        mOptions.inSampleSize = (1 << level);
449        mOptions.inBitmap = bitmap;
450
451        try {
452            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
453        } finally {
454            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
455                mOptions.inBitmap = null;
456            }
457        }
458
459        if (bitmap == null) {
460            Log.w("BitmapRegionTileSource", "fail in decoding region");
461        }
462        return bitmap;
463    }
464
465    private Bitmap getTileWithoutReusingBitmap(
466            int level, int x, int y, int tileSize) {
467
468        int t = tileSize << level;
469        mWantRegion.set(x, y, x + t, y + t);
470
471        mOverlapRegion.set(0, 0, mWidth, mHeight);
472
473        mOptions.inSampleSize = (1 << level);
474        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
475
476        if (bitmap == null) {
477            Log.w(TAG, "fail in decoding region");
478        }
479
480        if (mWantRegion.equals(mOverlapRegion)) {
481            return bitmap;
482        }
483
484        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
485        if (mCanvas == null) {
486            mCanvas = new Canvas();
487        }
488        mCanvas.setBitmap(result);
489        mCanvas.drawBitmap(bitmap,
490                (mOverlapRegion.left - mWantRegion.left) >> level,
491                (mOverlapRegion.top - mWantRegion.top) >> level, null);
492        mCanvas.setBitmap(null);
493        return result;
494    }
495
496    /**
497     * Note that the returned bitmap may have a long edge that's longer
498     * than the targetSize, but it will always be less than 2x the targetSize
499     */
500    private Bitmap decodePreview(BitmapSource source, int targetSize) {
501        Bitmap result = source.getPreviewBitmap();
502        if (result == null) {
503            return null;
504        }
505
506        // We need to resize down if the decoder does not support inSampleSize
507        // or didn't support the specified inSampleSize (some decoders only do powers of 2)
508        float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
509
510        if (scale <= 0.5) {
511            result = BitmapUtils.resizeBitmapByScale(result, scale, true);
512        }
513        return ensureGLCompatibleBitmap(result);
514    }
515
516    private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
517        if (bitmap == null || bitmap.getConfig() != null) {
518            return bitmap;
519        }
520        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
521        bitmap.recycle();
522        return newBitmap;
523    }
524}
525