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