ImageLoader.java revision 13b2fd3ab7ff65ac5c18b4c9de69062f3a549669
1/*
2 * Copyright (C) 2012 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.gallery3d.filtershow.cache;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteException;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.BitmapRegionDecoder;
27import android.graphics.Matrix;
28import android.graphics.Rect;
29import android.media.ExifInterface;
30import android.net.Uri;
31import android.provider.MediaStore;
32import android.util.Log;
33
34import com.adobe.xmp.XMPException;
35import com.adobe.xmp.XMPMeta;
36
37import com.android.gallery3d.R;
38import com.android.gallery3d.common.Utils;
39import com.android.gallery3d.filtershow.FilterShowActivity;
40import com.android.gallery3d.filtershow.HistoryAdapter;
41import com.android.gallery3d.filtershow.imageshow.ImageCrop;
42import com.android.gallery3d.filtershow.imageshow.ImageShow;
43import com.android.gallery3d.filtershow.presets.ImagePreset;
44import com.android.gallery3d.filtershow.tools.SaveCopyTask;
45import com.android.gallery3d.util.XmpUtilHelper;
46
47import java.io.Closeable;
48import java.io.File;
49import java.io.FileNotFoundException;
50import java.io.IOException;
51import java.io.InputStream;
52import java.util.Vector;
53
54public class ImageLoader {
55
56    private static final String LOGTAG = "ImageLoader";
57    private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
58    private Bitmap mOriginalBitmapSmall = null;
59    private Bitmap mOriginalBitmapLarge = null;
60    private Bitmap mBackgroundBitmap = null;
61
62    private Cache mCache = null;
63    private Cache mHiresCache = null;
64    private final ZoomCache mZoomCache = new ZoomCache();
65
66    private int mOrientation = 0;
67    private HistoryAdapter mAdapter = null;
68
69    private FilterShowActivity mActivity = null;
70
71    private static final int ORI_NORMAL     = ExifInterface.ORIENTATION_NORMAL;
72    private static final int ORI_ROTATE_90  = ExifInterface.ORIENTATION_ROTATE_90;
73    private static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
74    private static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
75    private static final int ORI_FLIP_HOR   = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
76    private static final int ORI_FLIP_VERT  = ExifInterface.ORIENTATION_FLIP_VERTICAL;
77    private static final int ORI_TRANSPOSE  = ExifInterface.ORIENTATION_TRANSPOSE;
78    private static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
79
80    private Context mContext = null;
81    private Uri mUri = null;
82
83    private Rect mOriginalBounds = null;
84
85    public ImageLoader(FilterShowActivity activity, Context context) {
86        mActivity = activity;
87        mContext = context;
88        mCache = new DelayedPresetCache(this, 30);
89        mHiresCache = new DelayedPresetCache(this, 3);
90    }
91
92    public void loadBitmap(Uri uri,int size) {
93        mUri = uri;
94        mOrientation = getOrientation(mContext, uri);
95        mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
96        if (mOriginalBitmapSmall == null) {
97            // Couldn't read the bitmap, let's exit
98            mActivity.cannotLoadImage();
99        }
100        mOriginalBitmapLarge = loadScaledBitmap(uri, size);
101        updateBitmaps();
102    }
103
104    public Uri getUri() {
105        return mUri;
106    }
107
108    public Rect getOriginalBounds() {
109        return mOriginalBounds;
110    }
111
112    public static int getOrientation(Context context, Uri uri) {
113        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
114            return getOrientationFromPath(uri.getPath());
115        }
116
117        Cursor cursor = null;
118        try {
119            cursor = context.getContentResolver().query(uri,
120                    new String[] {
121                        MediaStore.Images.ImageColumns.ORIENTATION
122                    },
123                    null, null, null);
124            if (cursor.moveToNext()){
125              int ori =   cursor.getInt(0);
126
127              switch (ori){
128                  case 0:   return ORI_NORMAL;
129                  case 90:  return ORI_ROTATE_90;
130                  case 270: return ORI_ROTATE_270;
131                  case 180: return ORI_ROTATE_180;
132                  default:
133                      return -1;
134              }
135            } else{
136                return -1;
137            }
138        } catch (SQLiteException e){
139            return ExifInterface.ORIENTATION_UNDEFINED;
140        } finally {
141            Utils.closeSilently(cursor);
142        }
143    }
144
145    static int getOrientationFromPath(String path) {
146        int orientation = -1;
147        try {
148            ExifInterface EXIF = new ExifInterface(path);
149            orientation = EXIF.getAttributeInt(ExifInterface.TAG_ORIENTATION,
150                    1);
151        } catch (IOException e) {
152            e.printStackTrace();
153        }
154        return orientation;
155    }
156
157    private void updateBitmaps() {
158        if (mOrientation > 1) {
159            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
160            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
161        }
162        mCache.setOriginalBitmap(mOriginalBitmapSmall);
163        mHiresCache.setOriginalBitmap(mOriginalBitmapLarge);
164        warnListeners();
165    }
166
167    public static Bitmap rotateToPortrait(Bitmap bitmap,int ori) {
168           Matrix matrix = new Matrix();
169           int w = bitmap.getWidth();
170           int h = bitmap.getHeight();
171           if (ori == ORI_ROTATE_90 ||
172                   ori == ORI_ROTATE_270 ||
173                   ori == ORI_TRANSPOSE||
174                   ori == ORI_TRANSVERSE) {
175               int tmp = w;
176               w = h;
177               h = tmp;
178           }
179           switch(ori){
180               case ORI_ROTATE_90:
181                   matrix.setRotate(90,w/2f,h/2f);
182                   break;
183               case ORI_ROTATE_180:
184                   matrix.setRotate(180,w/2f,h/2f);
185                   break;
186               case ORI_ROTATE_270:
187                   matrix.setRotate(270,w/2f,h/2f);
188                   break;
189               case ORI_FLIP_HOR:
190                   matrix.preScale(-1, 1);
191                   break;
192              case ORI_FLIP_VERT:
193                   matrix.preScale(1, -1);
194                   break;
195               case ORI_TRANSPOSE:
196                   matrix.setRotate(90,w/2f,h/2f);
197                   matrix.preScale(1, -1);
198                   break;
199               case ORI_TRANSVERSE:
200                   matrix.setRotate(270,w/2f,h/2f);
201                   matrix.preScale(1, -1);
202                   break;
203               case ORI_NORMAL:
204               default:
205                   return bitmap;
206            }
207
208        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
209                bitmap.getHeight(), matrix, true);
210    }
211
212    private void closeStream(Closeable stream) {
213        if (stream != null) {
214            try {
215                stream.close();
216            } catch (IOException e) {
217                e.printStackTrace();
218            }
219        }
220    }
221
222    private Bitmap loadRegionBitmap(Uri uri, Rect bounds) {
223        InputStream is = null;
224        try {
225            is = mContext.getContentResolver().openInputStream(uri);
226            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
227            return decoder.decodeRegion(bounds, null);
228        } catch (FileNotFoundException e) {
229            Log.e(LOGTAG, "FileNotFoundException: " + uri);
230        } catch (Exception e) {
231            e.printStackTrace();
232        } finally {
233            closeStream(is);
234        }
235        return null;
236    }
237
238    static final int MAX_BITMAP_DIM = 2048;
239    private Bitmap loadScaledBitmap(Uri uri, int size) {
240        InputStream is = null;
241        try {
242            is = mContext.getContentResolver().openInputStream(uri);
243            Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
244                    + is);
245            BitmapFactory.Options o = new BitmapFactory.Options();
246            o.inJustDecodeBounds = true;
247            BitmapFactory.decodeStream(is, null, o);
248
249            int width_tmp = o.outWidth;
250            int height_tmp = o.outHeight;
251
252            mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
253
254            int scale = 1;
255            while (true) {
256                if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
257                    if (width_tmp / 2 < size || height_tmp / 2 < size) {
258                        break;
259                    }
260                }
261                width_tmp /= 2;
262                height_tmp /= 2;
263                scale *= 2;
264            }
265
266            // decode with inSampleSize
267            BitmapFactory.Options o2 = new BitmapFactory.Options();
268            o2.inSampleSize = scale;
269
270            closeStream(is);
271            is = mContext.getContentResolver().openInputStream(uri);
272            return BitmapFactory.decodeStream(is, null, o2);
273        } catch (FileNotFoundException e) {
274            Log.e(LOGTAG, "FileNotFoundException: " + uri);
275        } catch (Exception e) {
276            e.printStackTrace();
277        } finally {
278            closeStream(is);
279        }
280        return null;
281    }
282
283    public Bitmap getBackgroundBitmap(Resources resources) {
284        if (mBackgroundBitmap == null) {
285            mBackgroundBitmap = BitmapFactory.decodeResource(resources,
286                    R.drawable.filtershow_background);
287        }
288        return mBackgroundBitmap;
289
290    }
291
292    public Bitmap getOriginalBitmapSmall() {
293        return mOriginalBitmapSmall;
294    }
295
296    public Bitmap getOriginalBitmapLarge() {
297        return mOriginalBitmapLarge;
298    }
299
300    public void addListener(ImageShow imageShow) {
301        if (!mListeners.contains(imageShow)) {
302            mListeners.add(imageShow);
303        }
304    }
305
306    private void warnListeners() {
307        mActivity.runOnUiThread(mWarnListenersRunnable);
308    }
309
310    private Runnable mWarnListenersRunnable = new Runnable() {
311
312        @Override
313        public void run() {
314            for (int i = 0; i < mListeners.size(); i++) {
315                ImageShow imageShow = mListeners.elementAt(i);
316                imageShow.imageLoaded();
317            }
318        }
319    };
320
321    // TODO: this currently does the loading + filtering on the UI thread -- need to
322    // move this to a background thread.
323    public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds,
324            boolean force) {
325        Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
326        if (force || bmp == null) {
327            bmp = loadRegionBitmap(mUri, bounds);
328            if (bmp != null) {
329                // TODO: this workaround for RS might not be needed ultimately
330                Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true);
331                float scaleFactor = imagePreset.getScaleFactor();
332                imagePreset.setScaleFactor(1.0f);
333                bmp2 = imagePreset.apply(bmp2);
334                imagePreset.setScaleFactor(scaleFactor);
335                mZoomCache.setImage(imagePreset, bounds, bmp2);
336                return bmp2;
337            }
338        }
339        return bmp;
340    }
341
342    // Caching method
343    public Bitmap getImageForPreset(ImageShow caller, ImagePreset imagePreset,
344            boolean hiRes) {
345        if (mOriginalBitmapSmall == null) {
346            return null;
347        }
348        if (mOriginalBitmapLarge == null) {
349            return null;
350        }
351
352        Bitmap filteredImage = null;
353
354        if (hiRes) {
355            filteredImage = mHiresCache.get(imagePreset);
356        } else {
357            filteredImage = mCache.get(imagePreset);
358        }
359
360        if (filteredImage == null) {
361            if (hiRes) {
362                mHiresCache.prepare(imagePreset);
363                mHiresCache.addObserver(caller);
364            } else {
365                mCache.prepare(imagePreset);
366                mCache.addObserver(caller);
367            }
368        }
369        return filteredImage;
370    }
371
372    public void resetImageForPreset(ImagePreset imagePreset, ImageShow caller) {
373        mHiresCache.reset(imagePreset);
374        mCache.reset(imagePreset);
375        mZoomCache.reset(imagePreset);
376    }
377
378    public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
379            File destination) {
380        preset.setIsHighQuality(true);
381        preset.setScaleFactor(1.0f);
382        new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
383
384            @Override
385            public void onComplete(Uri result) {
386                filterShowActivity.completeSaveImage(result);
387            }
388
389        }).execute(preset);
390    }
391
392    public void setAdapter(HistoryAdapter adapter) {
393        mAdapter = adapter;
394    }
395
396    public HistoryAdapter getHistory() {
397        return mAdapter;
398    }
399
400    public XMPMeta getXmpObject() {
401        try {
402            InputStream is = mContext.getContentResolver().openInputStream(getUri());
403            return XmpUtilHelper.extractXMPMeta(is);
404        } catch (FileNotFoundException e) {
405            return null;
406        }
407    }
408
409    /**
410     * Determine if this is a light cycle 360 image
411     *
412     * @return true if it is a light Cycle image that is full 360
413     */
414    public boolean queryLightCycle360() {
415        try {
416            InputStream is = mContext.getContentResolver().openInputStream(getUri());
417            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
418            if (meta == null) {
419                return false;
420            }
421            String name = meta.getPacketHeader();
422            try {
423                String namespace = "http://ns.google.com/photos/1.0/panorama/";
424                String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
425                String fullWidthName = "GPano:FullPanoWidthPixels";
426
427                if (!meta.doesPropertyExist(namespace, cropWidthName)) {
428                    return false;
429                }
430                if (!meta.doesPropertyExist(namespace, fullWidthName)) {
431                    return false;
432                }
433
434                Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
435                Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
436
437                // Definition of a 360:
438                // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
439                if (cropValue != null && fullValue != null) {
440                    return cropValue.equals(fullValue);
441                }
442
443                return false;
444            } catch (XMPException e) {
445                return false;
446            }
447        } catch (FileNotFoundException e) {
448            return false;
449        }
450    }
451
452}
453