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.BitmapFactory;
24import android.graphics.BitmapRegionDecoder;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.net.Uri;
29import android.opengl.GLUtils;
30import android.os.Build;
31import android.util.Log;
32
33import com.android.gallery3d.common.ExifOrientation;
34import com.android.gallery3d.common.Utils;
35import com.android.gallery3d.glrenderer.BasicTexture;
36import com.android.gallery3d.glrenderer.BitmapTexture;
37import com.android.photos.views.TiledImageRenderer;
38import com.android.wallpaperpicker.common.InputStreamProvider;
39
40import java.io.File;
41import java.io.IOException;
42import java.io.InputStream;
43
44interface SimpleBitmapRegionDecoder {
45    int getWidth();
46    int getHeight();
47    Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
48}
49
50class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
51    BitmapRegionDecoder mDecoder;
52    private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
53        mDecoder = decoder;
54    }
55
56    public static SimpleBitmapRegionDecoderWrapper newInstance(
57            InputStream is, boolean isShareable) {
58        try {
59            BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
60            if (d != null) {
61                return new SimpleBitmapRegionDecoderWrapper(d);
62            }
63        } catch (IOException e) {
64            Log.w("BitmapRegionTileSource", "getting decoder failed", e);
65            return null;
66        }
67        return null;
68    }
69    public int getWidth() {
70        return mDecoder.getWidth();
71    }
72    public int getHeight() {
73        return mDecoder.getHeight();
74    }
75    public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
76        return mDecoder.decodeRegion(wantRegion, options);
77    }
78}
79
80class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
81    Bitmap mBuffer;
82    Canvas mTempCanvas;
83    Paint mTempPaint;
84    private DumbBitmapRegionDecoder(Bitmap b) {
85        mBuffer = b;
86    }
87    public static DumbBitmapRegionDecoder newInstance(InputStream is) {
88        Bitmap b = BitmapFactory.decodeStream(is);
89        if (b != null) {
90            return new DumbBitmapRegionDecoder(b);
91        }
92        return null;
93    }
94    public int getWidth() {
95        return mBuffer.getWidth();
96    }
97    public int getHeight() {
98        return mBuffer.getHeight();
99    }
100    public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
101        if (mTempCanvas == null) {
102            mTempCanvas = new Canvas();
103            mTempPaint = new Paint();
104            mTempPaint.setFilterBitmap(true);
105        }
106        int sampleSize = Math.max(options.inSampleSize, 1);
107        Bitmap newBitmap = Bitmap.createBitmap(
108                wantRegion.width() / sampleSize,
109                wantRegion.height() / sampleSize,
110                Bitmap.Config.ARGB_8888);
111        mTempCanvas.setBitmap(newBitmap);
112        mTempCanvas.save();
113        mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
114        mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
115        mTempCanvas.restore();
116        mTempCanvas.setBitmap(null);
117        return newBitmap;
118    }
119}
120
121/**
122 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
123 * {@link BitmapRegionDecoder} to wrap a local file
124 */
125@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
126public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
127
128    private static final String TAG = "BitmapRegionTileSource";
129
130    private static final int GL_SIZE_LIMIT = 2048;
131    // This must be no larger than half the size of the GL_SIZE_LIMIT
132    // due to decodePreview being allowed to be up to 2x the size of the target
133    private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
134
135    public static abstract class BitmapSource {
136        private SimpleBitmapRegionDecoder mDecoder;
137        private Bitmap mPreview;
138        private int mRotation;
139        public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
140        private State mState = State.NOT_LOADED;
141
142        /** Returns whether loading was successful. */
143        public boolean loadInBackground(InBitmapProvider bitmapProvider) {
144            mRotation = getExifRotation();
145            mDecoder = loadBitmapRegionDecoder();
146            if (mDecoder == null) {
147                mState = State.ERROR_LOADING;
148                return false;
149            } else {
150                int width = mDecoder.getWidth();
151                int height = mDecoder.getHeight();
152
153                BitmapFactory.Options opts = new BitmapFactory.Options();
154                opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
155                opts.inPreferQualityOverSpeed = true;
156
157                float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height);
158                opts.inSampleSize = Utils.computeSampleSizeLarger(scale);
159                opts.inJustDecodeBounds = false;
160                opts.inMutable = true;
161
162                if (bitmapProvider != null) {
163                    int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
164                    Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
165                    if (reusableBitmap != null) {
166                        // Try loading with reusable bitmap
167                        opts.inBitmap = reusableBitmap;
168                        try {
169                            mPreview = loadPreviewBitmap(opts);
170                        } catch (IllegalArgumentException e) {
171                            Log.d(TAG, "Unable to reuse bitmap", e);
172                            opts.inBitmap = null;
173                            mPreview = null;
174                        }
175                    }
176                }
177                if (mPreview == null) {
178                    mPreview = loadPreviewBitmap(opts);
179                }
180                if (mPreview == null) {
181                    mState = State.ERROR_LOADING;
182                    return false;
183                }
184
185                // Verify that the bitmap can be used on GL surface
186                try {
187                    GLUtils.getInternalFormat(mPreview);
188                    GLUtils.getType(mPreview);
189                    mState = State.LOADED;
190                } catch (IllegalArgumentException e) {
191                    Log.d(TAG, "Image cannot be rendered on a GL surface", e);
192                    mState = State.ERROR_LOADING;
193                }
194                return mState == State.LOADED;
195            }
196        }
197
198        public State getLoadingState() {
199            return mState;
200        }
201
202        public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
203            return mDecoder;
204        }
205
206        public Bitmap getPreviewBitmap() {
207            return mPreview;
208        }
209
210        public int getRotation() {
211            return mRotation;
212        }
213
214        public abstract int getExifRotation();
215        public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
216        public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
217
218        public interface InBitmapProvider {
219            Bitmap forPixelCount(int count);
220        }
221    }
222
223    public static class InputStreamSource extends BitmapSource {
224        private final InputStreamProvider mStreamProvider;
225        private final Context mContext;
226
227        public InputStreamSource(Context context, Uri uri) {
228            this(InputStreamProvider.fromUri(context, uri), context);
229        }
230
231        public InputStreamSource(Resources res, int resId, Context context) {
232            this(InputStreamProvider.fromResource(res, resId), context);
233        }
234
235        public InputStreamSource(InputStreamProvider streamProvider, Context context) {
236            mStreamProvider = streamProvider;
237            mContext = context;
238        }
239
240        @Override
241        public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
242            try {
243                InputStream is = mStreamProvider.newStreamNotNull();
244                SimpleBitmapRegionDecoder regionDecoder =
245                        SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
246                Utils.closeSilently(is);
247                if (regionDecoder == null) {
248                    is = mStreamProvider.newStreamNotNull();
249                    regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
250                    Utils.closeSilently(is);
251                }
252                return regionDecoder;
253            } catch (IOException e) {
254                Log.e("InputStreamSource", "Failed to load stream", e);
255                return null;
256            }
257        }
258
259        @Override
260        public int getExifRotation() {
261            return mStreamProvider.getRotationFromExif(mContext);
262        }
263
264        @Override
265        public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
266            try {
267                InputStream is = mStreamProvider.newStreamNotNull();
268                Bitmap b = BitmapFactory.decodeStream(is, null, options);
269                Utils.closeSilently(is);
270                return b;
271            } catch (IOException | OutOfMemoryError e) {
272                Log.e("InputStreamSource", "Failed to load stream", e);
273                return null;
274            }
275        }
276    }
277
278    public static class FilePathBitmapSource extends InputStreamSource {
279        private String mPath;
280        public FilePathBitmapSource(File file, Context context) {
281            super(context, Uri.fromFile(file));
282            mPath = file.getAbsolutePath();
283        }
284
285        @Override
286        public int getExifRotation() {
287            return ExifOrientation.readRotation(mPath);
288        }
289    }
290
291    SimpleBitmapRegionDecoder mDecoder;
292    int mWidth;
293    int mHeight;
294    int mTileSize;
295    private BasicTexture mPreview;
296    private final int mRotation;
297
298    // For use only by getTile
299    private Rect mWantRegion = new Rect();
300    private BitmapFactory.Options mOptions;
301
302    public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
303        mTileSize = TiledImageRenderer.suggestedTileSize(context);
304        mRotation = source.getRotation();
305        mDecoder = source.getBitmapRegionDecoder();
306        if (mDecoder != null) {
307            mWidth = mDecoder.getWidth();
308            mHeight = mDecoder.getHeight();
309            mOptions = new BitmapFactory.Options();
310            mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
311            mOptions.inPreferQualityOverSpeed = true;
312            mOptions.inTempStorage = tempStorage;
313
314            Bitmap preview = source.getPreviewBitmap();
315            if (preview != null &&
316                    preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
317                    mPreview = new BitmapTexture(preview);
318            } else {
319                Log.w(TAG, String.format(
320                        "Failed to create preview of apropriate size! "
321                        + " in: %dx%d, out: %dx%d",
322                        mWidth, mHeight,
323                        preview == null ? -1 : preview.getWidth(),
324                        preview == null ? -1 : preview.getHeight()));
325            }
326        }
327    }
328
329    public Bitmap getBitmap() {
330        return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
331    }
332
333    @Override
334    public int getTileSize() {
335        return mTileSize;
336    }
337
338    @Override
339    public int getImageWidth() {
340        return mWidth;
341    }
342
343    @Override
344    public int getImageHeight() {
345        return mHeight;
346    }
347
348    @Override
349    public BasicTexture getPreview() {
350        return mPreview;
351    }
352
353    @Override
354    public int getRotation() {
355        return mRotation;
356    }
357
358    @Override
359    public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
360        int tileSize = getTileSize();
361        int t = tileSize << level;
362        mWantRegion.set(x, y, x + t, y + t);
363
364        if (bitmap == null) {
365            bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
366        }
367
368        mOptions.inSampleSize = (1 << level);
369        mOptions.inBitmap = bitmap;
370
371        try {
372            bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
373        } finally {
374            if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
375                mOptions.inBitmap = null;
376            }
377        }
378
379        if (bitmap == null) {
380            Log.w("BitmapRegionTileSource", "fail in decoding region");
381        }
382        return bitmap;
383    }
384}
385