ImageLoader.java revision 795776f67fe5bca902b09c01edec630879263341
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.Intent;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteException;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.BitmapRegionDecoder;
28import android.graphics.Matrix;
29import android.graphics.Rect;
30import android.graphics.Bitmap.CompressFormat;
31import android.media.ExifInterface;
32import android.net.Uri;
33import android.provider.MediaStore;
34import android.util.Log;
35
36import com.adobe.xmp.XMPException;
37import com.adobe.xmp.XMPMeta;
38
39import com.android.gallery3d.R;
40import com.android.gallery3d.common.Utils;
41import com.android.gallery3d.exif.ExifInvalidFormatException;
42import com.android.gallery3d.exif.ExifParser;
43import com.android.gallery3d.exif.ExifTag;
44import com.android.gallery3d.filtershow.CropExtras;
45import com.android.gallery3d.filtershow.FilterShowActivity;
46import com.android.gallery3d.filtershow.HistoryAdapter;
47import com.android.gallery3d.filtershow.imageshow.ImageCrop;
48import com.android.gallery3d.filtershow.imageshow.ImageShow;
49import com.android.gallery3d.filtershow.presets.ImagePreset;
50import com.android.gallery3d.filtershow.tools.BitmapTask;
51import com.android.gallery3d.filtershow.tools.SaveCopyTask;
52import com.android.gallery3d.util.InterruptableOutputStream;
53import com.android.gallery3d.util.XmpUtilHelper;
54
55import java.io.Closeable;
56import java.io.File;
57import java.io.FileInputStream;
58import java.io.FileNotFoundException;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.OutputStream;
62import java.util.Vector;
63import java.util.concurrent.locks.ReentrantLock;
64
65public class ImageLoader {
66
67    private static final String LOGTAG = "ImageLoader";
68    private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
69    private Bitmap mOriginalBitmapSmall = null;
70    private Bitmap mOriginalBitmapLarge = null;
71    private Bitmap mBackgroundBitmap = null;
72
73    private Cache mCache = null;
74    private Cache mHiresCache = null;
75    private final ZoomCache mZoomCache = new ZoomCache();
76
77    private int mOrientation = 0;
78    private HistoryAdapter mAdapter = null;
79
80    private FilterShowActivity mActivity = null;
81
82    public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
83    public static final int DEFAULT_COMPRESS_QUALITY = 95;
84
85    public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL;
86    public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90;
87    public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180;
88    public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270;
89    public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
90    public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL;
91    public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE;
92    public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE;
93
94    private Context mContext = null;
95    private Uri mUri = null;
96
97    private Rect mOriginalBounds = null;
98    private static int mZoomOrientation = ORI_NORMAL;
99
100    private ReentrantLock mLoadingLock = new ReentrantLock();
101
102    public ImageLoader(FilterShowActivity activity, Context context) {
103        mActivity = activity;
104        mContext = context;
105        mCache = new DelayedPresetCache(this, 30);
106        mHiresCache = new DelayedPresetCache(this, 3);
107    }
108
109    public static int getZoomOrientation() {
110        return mZoomOrientation;
111    }
112
113    public FilterShowActivity getActivity() {
114        return mActivity;
115    }
116
117    public boolean loadBitmap(Uri uri, int size) {
118        mLoadingLock.lock();
119        mUri = uri;
120        mOrientation = getOrientation(mContext, uri);
121        mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
122        if (mOriginalBitmapSmall == null) {
123            // Couldn't read the bitmap, let's exit
124            mLoadingLock.unlock();
125            return false;
126        }
127        mOriginalBitmapLarge = loadScaledBitmap(uri, size);
128        if (mOriginalBitmapLarge == null) {
129            mLoadingLock.unlock();
130            return false;
131        }
132        updateBitmaps();
133        mLoadingLock.unlock();
134        return true;
135    }
136
137    public Uri getUri() {
138        return mUri;
139    }
140
141    public Rect getOriginalBounds() {
142        return mOriginalBounds;
143    }
144
145    public static int getOrientation(Context context, Uri uri) {
146        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
147            return getOrientationFromPath(uri.getPath());
148        }
149
150        Cursor cursor = null;
151        try {
152            cursor = context.getContentResolver().query(uri,
153                    new String[] {
154                        MediaStore.Images.ImageColumns.ORIENTATION
155                    },
156                    null, null, null);
157            if (cursor.moveToNext()) {
158                int ori = cursor.getInt(0);
159
160                switch (ori) {
161                    case 0:
162                        return ORI_NORMAL;
163                    case 90:
164                        return ORI_ROTATE_90;
165                    case 270:
166                        return ORI_ROTATE_270;
167                    case 180:
168                        return ORI_ROTATE_180;
169                    default:
170                        return -1;
171                }
172            } else {
173                return -1;
174            }
175        } catch (SQLiteException e) {
176            return ExifInterface.ORIENTATION_UNDEFINED;
177        } catch (IllegalArgumentException e) {
178            return ExifInterface.ORIENTATION_UNDEFINED;
179        } finally {
180            Utils.closeSilently(cursor);
181        }
182    }
183
184    static int getOrientationFromPath(String path) {
185        int orientation = -1;
186        InputStream is = null;
187        try {
188            is = new FileInputStream(path);
189            ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0);
190            int event = parser.next();
191            while (event != ExifParser.EVENT_END) {
192                if (event == ExifParser.EVENT_NEW_TAG) {
193                    ExifTag tag = parser.getTag();
194                    if (tag.getTagId() == ExifTag.TAG_ORIENTATION) {
195                        orientation = (int) tag.getValueAt(0);
196                        break;
197                    }
198                }
199                event = parser.next();
200            }
201        } catch (IOException e) {
202            e.printStackTrace();
203        } catch (ExifInvalidFormatException e) {
204            e.printStackTrace();
205        } finally {
206            Utils.closeSilently(is);
207        }
208        return orientation;
209    }
210
211    private void updateBitmaps() {
212        if (mOrientation > 1) {
213            mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
214            mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
215        }
216        mZoomOrientation = mOrientation;
217        mCache.setOriginalBitmap(mOriginalBitmapSmall);
218        mHiresCache.setOriginalBitmap(mOriginalBitmapLarge);
219        warnListeners();
220    }
221
222    public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) {
223        Matrix matrix = new Matrix();
224        int w = bitmap.getWidth();
225        int h = bitmap.getHeight();
226        if (ori == ORI_ROTATE_90 ||
227                ori == ORI_ROTATE_270 ||
228                ori == ORI_TRANSPOSE ||
229                ori == ORI_TRANSVERSE) {
230            int tmp = w;
231            w = h;
232            h = tmp;
233        }
234        switch (ori) {
235            case ORI_ROTATE_90:
236                matrix.setRotate(90, w / 2f, h / 2f);
237                break;
238            case ORI_ROTATE_180:
239                matrix.setRotate(180, w / 2f, h / 2f);
240                break;
241            case ORI_ROTATE_270:
242                matrix.setRotate(270, w / 2f, h / 2f);
243                break;
244            case ORI_FLIP_HOR:
245                matrix.preScale(-1, 1);
246                break;
247            case ORI_FLIP_VERT:
248                matrix.preScale(1, -1);
249                break;
250            case ORI_TRANSPOSE:
251                matrix.setRotate(90, w / 2f, h / 2f);
252                matrix.preScale(1, -1);
253                break;
254            case ORI_TRANSVERSE:
255                matrix.setRotate(270, w / 2f, h / 2f);
256                matrix.preScale(1, -1);
257                break;
258            case ORI_NORMAL:
259            default:
260                return bitmap;
261        }
262
263        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
264                bitmap.getHeight(), matrix, true);
265    }
266
267    private void closeStream(Closeable stream) {
268        if (stream != null) {
269            try {
270                stream.close();
271            } catch (IOException e) {
272                e.printStackTrace();
273            }
274        }
275    }
276
277    private Bitmap loadRegionBitmap(Uri uri, Rect bounds) {
278        InputStream is = null;
279        try {
280            is = mContext.getContentResolver().openInputStream(uri);
281            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
282            return decoder.decodeRegion(bounds, null);
283        } catch (FileNotFoundException e) {
284            Log.e(LOGTAG, "FileNotFoundException: " + uri);
285        } catch (Exception e) {
286            e.printStackTrace();
287        } finally {
288            closeStream(is);
289        }
290        return null;
291    }
292
293    static final int MAX_BITMAP_DIM = 2048;
294
295    private Bitmap loadScaledBitmap(Uri uri, int size) {
296        InputStream is = null;
297        try {
298            is = mContext.getContentResolver().openInputStream(uri);
299            Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
300                    + is);
301            BitmapFactory.Options o = new BitmapFactory.Options();
302            o.inJustDecodeBounds = true;
303            BitmapFactory.decodeStream(is, null, o);
304
305            int width_tmp = o.outWidth;
306            int height_tmp = o.outHeight;
307
308            mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
309
310            int scale = 1;
311            while (true) {
312                if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) {
313                    if (width_tmp / 2 < size || height_tmp / 2 < size) {
314                        break;
315                    }
316                }
317                width_tmp /= 2;
318                height_tmp /= 2;
319                scale *= 2;
320            }
321
322            // decode with inSampleSize
323            BitmapFactory.Options o2 = new BitmapFactory.Options();
324            o2.inSampleSize = scale;
325
326            closeStream(is);
327            is = mContext.getContentResolver().openInputStream(uri);
328            return BitmapFactory.decodeStream(is, null, o2);
329        } catch (FileNotFoundException e) {
330            Log.e(LOGTAG, "FileNotFoundException: " + uri);
331        } catch (Exception e) {
332            e.printStackTrace();
333        } finally {
334            closeStream(is);
335        }
336        return null;
337    }
338
339    public Bitmap getBackgroundBitmap(Resources resources) {
340        if (mBackgroundBitmap == null) {
341            mBackgroundBitmap = BitmapFactory.decodeResource(resources,
342                    R.drawable.filtershow_background);
343        }
344        return mBackgroundBitmap;
345
346    }
347
348    public Bitmap getOriginalBitmapSmall() {
349        return mOriginalBitmapSmall;
350    }
351
352    public Bitmap getOriginalBitmapLarge() {
353        return mOriginalBitmapLarge;
354    }
355
356    public void addListener(ImageShow imageShow) {
357        mLoadingLock.lock();
358        if (!mListeners.contains(imageShow)) {
359            mListeners.add(imageShow);
360        }
361        mHiresCache.addObserver(imageShow);
362        mLoadingLock.unlock();
363    }
364
365    private void warnListeners() {
366        mActivity.runOnUiThread(mWarnListenersRunnable);
367    }
368
369    private Runnable mWarnListenersRunnable = new Runnable() {
370
371        @Override
372        public void run() {
373            for (int i = 0; i < mListeners.size(); i++) {
374                ImageShow imageShow = mListeners.elementAt(i);
375                imageShow.imageLoaded();
376            }
377        }
378    };
379
380    // TODO: this currently does the loading + filtering on the UI thread --
381    // need to
382    // move this to a background thread.
383    public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds,
384            boolean force) {
385        mLoadingLock.lock();
386        Bitmap bmp = mZoomCache.getImage(imagePreset, bounds);
387        if (force || bmp == null) {
388            bmp = loadRegionBitmap(mUri, bounds);
389            if (bmp != null) {
390                // TODO: this workaround for RS might not be needed ultimately
391                Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true);
392                float scaleFactor = imagePreset.getScaleFactor();
393                imagePreset.setScaleFactor(1.0f);
394                bmp2 = imagePreset.apply(bmp2);
395                imagePreset.setScaleFactor(scaleFactor);
396                mZoomCache.setImage(imagePreset, bounds, bmp2);
397                mLoadingLock.unlock();
398                return bmp2;
399            }
400        }
401        mLoadingLock.unlock();
402        return bmp;
403    }
404
405    // Caching method
406    public Bitmap getImageForPreset(ImageShow caller, ImagePreset imagePreset,
407            boolean hiRes) {
408        mLoadingLock.lock();
409        if (mOriginalBitmapSmall == null) {
410            mLoadingLock.unlock();
411            return null;
412        }
413        if (mOriginalBitmapLarge == null) {
414            mLoadingLock.unlock();
415            return null;
416        }
417
418        Bitmap filteredImage = null;
419
420        if (hiRes) {
421            filteredImage = mHiresCache.get(imagePreset);
422        } else {
423            filteredImage = mCache.get(imagePreset);
424        }
425
426        if (filteredImage == null) {
427            if (hiRes) {
428                mHiresCache.prepare(imagePreset);
429                mHiresCache.addObserver(caller);
430            } else {
431                mCache.prepare(imagePreset);
432                mCache.addObserver(caller);
433            }
434        }
435        mLoadingLock.unlock();
436        return filteredImage;
437    }
438
439    public void resetImageForPreset(ImagePreset imagePreset, ImageShow caller) {
440        mLoadingLock.lock();
441        mHiresCache.reset(imagePreset);
442        mCache.reset(imagePreset);
443        mZoomCache.reset(imagePreset);
444        mLoadingLock.unlock();
445    }
446
447    public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
448            File destination) {
449        preset.setIsHighQuality(true);
450        preset.setScaleFactor(1.0f);
451        new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
452
453            @Override
454            public void onComplete(Uri result) {
455                filterShowActivity.completeSaveImage(result);
456            }
457
458        }).execute(preset);
459    }
460
461    public static Bitmap loadMutableBitmap(Context context, Uri sourceUri)
462            throws FileNotFoundException {
463        BitmapFactory.Options options = new BitmapFactory.Options();
464        // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
465        // exist)
466        options.inMutable = true;
467
468        InputStream is = context.getContentResolver().openInputStream(sourceUri);
469        Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
470        int orientation = ImageLoader.getOrientation(context, sourceUri);
471        bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
472        return bitmap;
473    }
474
475    public void returnFilteredResult(ImagePreset preset,
476            final FilterShowActivity filterShowActivity) {
477        preset.setIsHighQuality(true);
478        preset.setScaleFactor(1.0f);
479
480        BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
481
482            @Override
483            public void onComplete(Bitmap result) {
484                filterShowActivity.onFilteredResult(result);
485            }
486
487            @Override
488            public void onCancel() {
489            }
490
491            @Override
492            public Bitmap onExecute(ImagePreset param) {
493                if (param == null) {
494                    return null;
495                }
496                try {
497                    Bitmap bitmap = param.apply(loadMutableBitmap(mContext, mUri));
498                    return bitmap;
499                } catch (FileNotFoundException ex) {
500                    Log.w(LOGTAG, "Failed to save image!", ex);
501                    return null;
502                }
503            }
504        };
505
506        (new BitmapTask<ImagePreset>(cb)).execute(preset);
507    }
508
509    private String getFileExtension(String requestFormat) {
510        String outputFormat = (requestFormat == null)
511                ? "jpg"
512                : requestFormat;
513        outputFormat = outputFormat.toLowerCase();
514        return (outputFormat.equals("png") || outputFormat.equals("gif"))
515                ? "png" // We don't support gif compression.
516                : "jpg";
517    }
518
519    private CompressFormat convertExtensionToCompressFormat(String extension) {
520        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
521    }
522
523    public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
524            final FilterShowActivity filterShowActivity) {
525
526        OutputStream out = null;
527        try {
528            out = filterShowActivity.getContentResolver().openOutputStream(uri);
529        } catch (FileNotFoundException e) {
530            Log.w(LOGTAG, "cannot write output", e);
531            out = null;
532        } finally {
533            if (bmap == null || out == null) {
534                return;
535            }
536        }
537
538        final InterruptableOutputStream ios = new InterruptableOutputStream(out);
539
540        BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
541
542            @Override
543            public void onComplete(Bitmap result) {
544                filterShowActivity.done();
545            }
546
547            @Override
548            public void onCancel() {
549                ios.interrupt();
550            }
551
552            @Override
553            public Bitmap onExecute(Bitmap param) {
554                CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
555                param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
556                Utils.closeSilently(ios);
557                return null;
558            }
559        };
560
561        (new BitmapTask<Bitmap>(cb)).execute(bmap);
562    }
563
564    public void setAdapter(HistoryAdapter adapter) {
565        mAdapter = adapter;
566    }
567
568    public HistoryAdapter getHistory() {
569        return mAdapter;
570    }
571
572    public XMPMeta getXmpObject() {
573        try {
574            InputStream is = mContext.getContentResolver().openInputStream(getUri());
575            return XmpUtilHelper.extractXMPMeta(is);
576        } catch (FileNotFoundException e) {
577            return null;
578        }
579    }
580
581    /**
582     * Determine if this is a light cycle 360 image
583     *
584     * @return true if it is a light Cycle image that is full 360
585     */
586    public boolean queryLightCycle360() {
587        try {
588            InputStream is = mContext.getContentResolver().openInputStream(getUri());
589            XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
590            if (meta == null) {
591                return false;
592            }
593            String name = meta.getPacketHeader();
594            try {
595                String namespace = "http://ns.google.com/photos/1.0/panorama/";
596                String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
597                String fullWidthName = "GPano:FullPanoWidthPixels";
598
599                if (!meta.doesPropertyExist(namespace, cropWidthName)) {
600                    return false;
601                }
602                if (!meta.doesPropertyExist(namespace, fullWidthName)) {
603                    return false;
604                }
605
606                Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
607                Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
608
609                // Definition of a 360:
610                // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
611                if (cropValue != null && fullValue != null) {
612                    return cropValue.equals(fullValue);
613                }
614
615                return false;
616            } catch (XMPException e) {
617                return false;
618            }
619        } catch (FileNotFoundException e) {
620            return false;
621        }
622    }
623
624    public void addCacheListener(ImageShow imageShow) {
625        mHiresCache.addObserver(imageShow);
626        mCache.addObserver(imageShow);
627    }
628
629}
630