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