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 
17 package com.android.photos;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.BitmapRegionDecoder;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.net.Uri;
29 import android.opengl.GLUtils;
30 import android.os.Build;
31 import android.util.Log;
32 
33 import com.android.gallery3d.common.ExifOrientation;
34 import com.android.gallery3d.common.Utils;
35 import com.android.gallery3d.glrenderer.BasicTexture;
36 import com.android.gallery3d.glrenderer.BitmapTexture;
37 import com.android.photos.views.TiledImageRenderer;
38 import com.android.wallpaperpicker.common.InputStreamProvider;
39 
40 import java.io.File;
41 import java.io.IOException;
42 import java.io.InputStream;
43 
44 interface SimpleBitmapRegionDecoder {
getWidth()45     int getWidth();
getHeight()46     int getHeight();
decodeRegion(Rect wantRegion, BitmapFactory.Options options)47     Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
48 }
49 
50 class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
51     BitmapRegionDecoder mDecoder;
SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder)52     private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
53         mDecoder = decoder;
54     }
55 
newInstance( InputStream is, boolean isShareable)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     }
getWidth()69     public int getWidth() {
70         return mDecoder.getWidth();
71     }
getHeight()72     public int getHeight() {
73         return mDecoder.getHeight();
74     }
decodeRegion(Rect wantRegion, BitmapFactory.Options options)75     public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
76         return mDecoder.decodeRegion(wantRegion, options);
77     }
78 }
79 
80 class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
81     Bitmap mBuffer;
82     Canvas mTempCanvas;
83     Paint mTempPaint;
DumbBitmapRegionDecoder(Bitmap b)84     private DumbBitmapRegionDecoder(Bitmap b) {
85         mBuffer = b;
86     }
newInstance(InputStream is)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     }
getWidth()94     public int getWidth() {
95         return mBuffer.getWidth();
96     }
getHeight()97     public int getHeight() {
98         return mBuffer.getHeight();
99     }
decodeRegion(Rect wantRegion, BitmapFactory.Options options)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)
126 public 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. */
loadInBackground(InBitmapProvider bitmapProvider)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 
getLoadingState()198         public State getLoadingState() {
199             return mState;
200         }
201 
getBitmapRegionDecoder()202         public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
203             return mDecoder;
204         }
205 
getPreviewBitmap()206         public Bitmap getPreviewBitmap() {
207             return mPreview;
208         }
209 
getRotation()210         public int getRotation() {
211             return mRotation;
212         }
213 
getExifRotation()214         public abstract int getExifRotation();
loadBitmapRegionDecoder()215         public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
loadPreviewBitmap(BitmapFactory.Options options)216         public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
217 
218         public interface InBitmapProvider {
forPixelCount(int count)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 
InputStreamSource(Context context, Uri uri)227         public InputStreamSource(Context context, Uri uri) {
228             this(InputStreamProvider.fromUri(context, uri), context);
229         }
230 
InputStreamSource(Resources res, int resId, Context context)231         public InputStreamSource(Resources res, int resId, Context context) {
232             this(InputStreamProvider.fromResource(res, resId), context);
233         }
234 
InputStreamSource(InputStreamProvider streamProvider, Context context)235         public InputStreamSource(InputStreamProvider streamProvider, Context context) {
236             mStreamProvider = streamProvider;
237             mContext = context;
238         }
239 
240         @Override
loadBitmapRegionDecoder()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
getExifRotation()260         public int getExifRotation() {
261             return mStreamProvider.getRotationFromExif(mContext);
262         }
263 
264         @Override
loadPreviewBitmap(BitmapFactory.Options options)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;
FilePathBitmapSource(File file, Context context)280         public FilePathBitmapSource(File file, Context context) {
281             super(context, Uri.fromFile(file));
282             mPath = file.getAbsolutePath();
283         }
284 
285         @Override
getExifRotation()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 
BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage)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 
getBitmap()329     public Bitmap getBitmap() {
330         return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
331     }
332 
333     @Override
getTileSize()334     public int getTileSize() {
335         return mTileSize;
336     }
337 
338     @Override
getImageWidth()339     public int getImageWidth() {
340         return mWidth;
341     }
342 
343     @Override
getImageHeight()344     public int getImageHeight() {
345         return mHeight;
346     }
347 
348     @Override
getPreview()349     public BasicTexture getPreview() {
350         return mPreview;
351     }
352 
353     @Override
getRotation()354     public int getRotation() {
355         return mRotation;
356     }
357 
358     @Override
getTile(int level, int x, int y, Bitmap bitmap)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