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