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.graphics.Bitmap;
22import android.graphics.Bitmap.Config;
23import android.graphics.BitmapFactory;
24import android.graphics.BitmapRegionDecoder;
25import android.graphics.Canvas;
26import android.graphics.Rect;
27import android.os.Build;
28import android.os.Build.VERSION_CODES;
29import android.util.Log;
30
31import com.android.gallery3d.common.BitmapUtils;
32import com.android.gallery3d.glrenderer.BasicTexture;
33import com.android.gallery3d.glrenderer.BitmapTexture;
34import com.android.photos.views.TiledImageRenderer;
35
36import java.io.IOException;
37
38/**
39 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
40 * {@link BitmapRegionDecoder} to wrap a local file
41 */
42@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
43public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
44
45    private static final String TAG = "BitmapRegionTileSource";
46
47    private static final boolean REUSE_BITMAP =
48            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
49    private static final int GL_SIZE_LIMIT = 2048;
50    // This must be no larger than half the size of the GL_SIZE_LIMIT
51    // due to decodePreview being allowed to be up to 2x the size of the target
52    private static final int MAX_PREVIEW_SIZE = 1024;
53
54    BitmapRegionDecoder mDecoder;
55    int mWidth;
56    int mHeight;
57    int mTileSize;
58    private BasicTexture mPreview;
59    private final int mRotation;
60
61    // For use only by getTile
62    private Rect mWantRegion = new Rect();
63    private Rect mOverlapRegion = new Rect();
64    private BitmapFactory.Options mOptions;
65    private Canvas mCanvas;
66
67    public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
68        mTileSize = TiledImageRenderer.suggestedTileSize(context);
69        mRotation = rotation;
70        try {
71            mDecoder = BitmapRegionDecoder.newInstance(path, true);
72            mWidth = mDecoder.getWidth();
73            mHeight = mDecoder.getHeight();
74        } catch (IOException e) {
75            Log.w("BitmapRegionTileSource", "ctor failed", e);
76        }
77        mOptions = new BitmapFactory.Options();
78        mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
79        mOptions.inPreferQualityOverSpeed = true;
80        mOptions.inTempStorage = new byte[16 * 1024];
81        if (previewSize != 0) {
82            previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
83            // Although this is the same size as the Bitmap that is likely already
84            // loaded, the lifecycle is different and interactions are on a different
85            // thread. Thus to simplify, this source will decode its own bitmap.
86            Bitmap preview = decodePreview(path, previewSize);
87            if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
88                mPreview = new BitmapTexture(preview);
89            } else {
90                Log.w(TAG, String.format(
91                        "Failed to create preview of apropriate size! "
92                        + " in: %dx%d, out: %dx%d",
93                        mWidth, mHeight,
94                        preview.getWidth(), preview.getHeight()));
95            }
96        }
97    }
98
99    @Override
100    public int getTileSize() {
101        return mTileSize;
102    }
103
104    @Override
105    public int getImageWidth() {
106        return mWidth;
107    }
108
109    @Override
110    public int getImageHeight() {
111        return mHeight;
112    }
113
114    @Override
115    public BasicTexture getPreview() {
116        return mPreview;
117    }
118
119    @Override
120    public int getRotation() {
121        return mRotation;
122    }
123
124    @Override
125    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
126        int tileSize = getTileSize();
127        if (!REUSE_BITMAP) {
128            return getTileWithoutReusingBitmap(level, x, y, tileSize);
129        }
130
131        int t = tileSize << level;
132        mWantRegion.set(x, y, x + t, y + t);
133
134        if (bitmap == null) {
135            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
136        }
137
138        mOptions.inSampleSize = (1 << level);
139        mOptions.inBitmap = bitmap;
140
141        try {
142            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
143        } finally {
144            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
145                mOptions.inBitmap = null;
146            }
147        }
148
149        if (bitmap == null) {
150            Log.w("BitmapRegionTileSource", "fail in decoding region");
151        }
152        return bitmap;
153    }
154
155    private Bitmap getTileWithoutReusingBitmap(
156            int level, int x, int y, int tileSize) {
157
158        int t = tileSize << level;
159        mWantRegion.set(x, y, x + t, y + t);
160
161        mOverlapRegion.set(0, 0, mWidth, mHeight);
162
163        mOptions.inSampleSize = (1 << level);
164        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
165
166        if (bitmap == null) {
167            Log.w(TAG, "fail in decoding region");
168        }
169
170        if (mWantRegion.equals(mOverlapRegion)) {
171            return bitmap;
172        }
173
174        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
175        if (mCanvas == null) {
176            mCanvas = new Canvas();
177        }
178        mCanvas.setBitmap(result);
179        mCanvas.drawBitmap(bitmap,
180                (mOverlapRegion.left - mWantRegion.left) >> level,
181                (mOverlapRegion.top - mWantRegion.top) >> level, null);
182        mCanvas.setBitmap(null);
183        return result;
184    }
185
186    /**
187     * Note that the returned bitmap may have a long edge that's longer
188     * than the targetSize, but it will always be less than 2x the targetSize
189     */
190    private Bitmap decodePreview(String file, int targetSize) {
191        float scale = (float) targetSize / Math.max(mWidth, mHeight);
192        mOptions.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
193        mOptions.inJustDecodeBounds = false;
194
195        Bitmap result = BitmapFactory.decodeFile(file, mOptions);
196        if (result == null) {
197            return null;
198        }
199
200        // We need to resize down if the decoder does not support inSampleSize
201        // or didn't support the specified inSampleSize (some decoders only do powers of 2)
202        scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
203
204        if (scale <= 0.5) {
205            result = BitmapUtils.resizeBitmapByScale(result, scale, true);
206        }
207        return ensureGLCompatibleBitmap(result);
208    }
209
210    private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
211        if (bitmap == null || bitmap.getConfig() != null) {
212            return bitmap;
213        }
214        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
215        bitmap.recycle();
216        return newBitmap;
217    }
218}
219