UriImage.java revision 5273e8306410d19e2e2cf6234bfe0a05097be874
1/*
2 * Copyright (C) 2010 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.data;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.Bitmap.Config;
23import android.graphics.BitmapFactory.Options;
24import android.graphics.BitmapRegionDecoder;
25import android.net.Uri;
26import android.os.ParcelFileDescriptor;
27
28import com.android.gallery3d.app.GalleryApp;
29import com.android.gallery3d.common.BitmapUtils;
30import com.android.gallery3d.common.Utils;
31import com.android.gallery3d.util.Future;
32import com.android.gallery3d.util.FutureListener;
33import com.android.gallery3d.util.LightCycleHelper;
34import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
35import com.android.gallery3d.util.ThreadPool.CancelListener;
36import com.android.gallery3d.util.ThreadPool.Job;
37import com.android.gallery3d.util.ThreadPool.JobContext;
38
39import java.io.FileInputStream;
40import java.io.FileNotFoundException;
41import java.io.InputStream;
42import java.net.URI;
43import java.net.URL;
44
45public class UriImage extends MediaItem {
46    private static final String TAG = "UriImage";
47
48    private static final int STATE_INIT = 0;
49    private static final int STATE_DOWNLOADING = 1;
50    private static final int STATE_DOWNLOADED = 2;
51    private static final int STATE_ERROR = -1;
52
53    private final Uri mUri;
54    private final String mContentType;
55
56    private DownloadCache.Entry mCacheEntry;
57    private ParcelFileDescriptor mFileDescriptor;
58    private int mState = STATE_INIT;
59    private int mWidth;
60    private int mHeight;
61    private int mRotation;
62
63    private Object mLock = new Object();
64    private Future<PanoramaMetadata> mGetPanoMetadataTask;
65    private boolean mPanoramaMetadataInitialized;
66    private PanoramaMetadata mPanoramaMetadata;
67    private SupportedOperationsListener mListener;
68
69    private GalleryApp mApplication;
70
71    public UriImage(GalleryApp application, Path path, Uri uri, String contentType) {
72        super(path, nextVersionNumber());
73        mUri = uri;
74        mApplication = Utils.checkNotNull(application);
75        mContentType = contentType;
76    }
77
78    @Override
79    public Job<Bitmap> requestImage(int type) {
80        return new BitmapJob(type);
81    }
82
83    @Override
84    public Job<BitmapRegionDecoder> requestLargeImage() {
85        return new RegionDecoderJob();
86    }
87
88    private void openFileOrDownloadTempFile(JobContext jc) {
89        int state = openOrDownloadInner(jc);
90        synchronized (this) {
91            mState = state;
92            if (mState != STATE_DOWNLOADED) {
93                if (mFileDescriptor != null) {
94                    Utils.closeSilently(mFileDescriptor);
95                    mFileDescriptor = null;
96                }
97            }
98            notifyAll();
99        }
100    }
101
102    private int openOrDownloadInner(JobContext jc) {
103        String scheme = mUri.getScheme();
104        if (ContentResolver.SCHEME_CONTENT.equals(scheme)
105                || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
106                || ContentResolver.SCHEME_FILE.equals(scheme)) {
107            try {
108                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
109                    InputStream is = mApplication.getContentResolver()
110                            .openInputStream(mUri);
111                    mRotation = Exif.getOrientation(is);
112                    Utils.closeSilently(is);
113                }
114                mFileDescriptor = mApplication.getContentResolver()
115                        .openFileDescriptor(mUri, "r");
116                if (jc.isCancelled()) return STATE_INIT;
117                return STATE_DOWNLOADED;
118            } catch (FileNotFoundException e) {
119                Log.w(TAG, "fail to open: " + mUri, e);
120                return STATE_ERROR;
121            }
122        } else {
123            try {
124                URL url = new URI(mUri.toString()).toURL();
125                mCacheEntry = mApplication.getDownloadCache().download(jc, url);
126                if (jc.isCancelled()) return STATE_INIT;
127                if (mCacheEntry == null) {
128                    Log.w(TAG, "download failed " + url);
129                    return STATE_ERROR;
130                }
131                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
132                    InputStream is = new FileInputStream(mCacheEntry.cacheFile);
133                    mRotation = Exif.getOrientation(is);
134                    Utils.closeSilently(is);
135                }
136                mFileDescriptor = ParcelFileDescriptor.open(
137                        mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY);
138                return STATE_DOWNLOADED;
139            } catch (Throwable t) {
140                Log.w(TAG, "download error", t);
141                return STATE_ERROR;
142            }
143        }
144    }
145
146    private boolean prepareInputFile(JobContext jc) {
147        jc.setCancelListener(new CancelListener() {
148            @Override
149            public void onCancel() {
150                synchronized (this) {
151                    notifyAll();
152                }
153            }
154        });
155
156        while (true) {
157            synchronized (this) {
158                if (jc.isCancelled()) return false;
159                if (mState == STATE_INIT) {
160                    mState = STATE_DOWNLOADING;
161                    // Then leave the synchronized block and continue.
162                } else if (mState == STATE_ERROR) {
163                    return false;
164                } else if (mState == STATE_DOWNLOADED) {
165                    return true;
166                } else /* if (mState == STATE_DOWNLOADING) */ {
167                    try {
168                        wait();
169                    } catch (InterruptedException ex) {
170                        // ignored.
171                    }
172                    continue;
173                }
174            }
175            // This is only reached for STATE_INIT->STATE_DOWNLOADING
176            openFileOrDownloadTempFile(jc);
177        }
178    }
179
180    private class RegionDecoderJob implements Job<BitmapRegionDecoder> {
181        @Override
182        public BitmapRegionDecoder run(JobContext jc) {
183            if (!prepareInputFile(jc)) return null;
184            BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
185                    jc, mFileDescriptor.getFileDescriptor(), false);
186            mWidth = decoder.getWidth();
187            mHeight = decoder.getHeight();
188            return decoder;
189        }
190    }
191
192    private class BitmapJob implements Job<Bitmap> {
193        private int mType;
194
195        protected BitmapJob(int type) {
196            mType = type;
197        }
198
199        @Override
200        public Bitmap run(JobContext jc) {
201            if (!prepareInputFile(jc)) return null;
202            int targetSize = MediaItem.getTargetSize(mType);
203            Options options = new Options();
204            options.inPreferredConfig = Config.ARGB_8888;
205            Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
206                    mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
207
208            if (jc.isCancelled() || bitmap == null) {
209                return null;
210            }
211
212            if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
213                bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
214            } else {
215                bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
216            }
217            return bitmap;
218        }
219    }
220
221    @Override
222    public int getSupportedOperations() {
223        int supported = SUPPORT_EDIT | SUPPORT_SETAS;
224        if (isSharable()) supported |= SUPPORT_SHARE;
225        if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
226            supported |= SUPPORT_FULL_IMAGE;
227        }
228        if (mPanoramaMetadata != null && mPanoramaMetadata.mUsePanoramaViewer) {
229            supported |= SUPPORT_PANORAMA;
230            if (mPanoramaMetadata.mIsPanorama360) {
231                supported |= SUPPORT_PANORAMA360;
232                // disable destructive crop for 360 degree panorama
233                supported &= ~SUPPORT_CROP;
234            }
235        }
236        return supported;
237    }
238
239    @Override
240    public int getSupportedOperations(boolean getAll) {
241        synchronized (mLock) {
242            if (getAll && !mPanoramaMetadataInitialized) {
243                if (mGetPanoMetadataTask == null) {
244                    mGetPanoMetadataTask = getThreadPool().submit(
245                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
246                                getContentUri()));
247                }
248                mPanoramaMetadata = mGetPanoMetadataTask.get();
249                mPanoramaMetadataInitialized = true;
250            }
251        }
252        return getSupportedOperations();
253    }
254
255    @Override
256    public void setSupportedOperationsListener(SupportedOperationsListener l) {
257        synchronized (mLock) {
258            if (l != null) {
259                if (mGetPanoMetadataTask != null) {
260                    mGetPanoMetadataTask.cancel();
261                    mGetPanoMetadataTask = null;
262                }
263            } else {
264                if (mGetPanoMetadataTask == null) {
265                    mGetPanoMetadataTask = getThreadPool().submit(
266                            new PanoramaMetadataJob(mApplication.getAndroidContext(),
267                                getContentUri()),
268                            new FutureListener<PanoramaMetadata>() {
269                                @Override
270                        public void onFutureDone(Future<PanoramaMetadata> future) {
271                            mGetPanoMetadataTask = null;
272                            if (future.isCancelled()) return;
273                            mPanoramaMetadata = future.get();
274                            mPanoramaMetadataInitialized = true;
275                            if (mListener != null) {
276                                mListener.onChange(UriImage.this, getSupportedOperations());
277                            }
278                        }
279                        });
280                }
281            }
282            mListener = l;
283        }
284    }
285
286    private boolean isSharable() {
287        // We cannot grant read permission to the receiver since we put
288        // the data URI in EXTRA_STREAM instead of the data part of an intent
289        // And there are issues in MediaUploader and Bluetooth file sender to
290        // share a general image data. So, we only share for local file.
291        return ContentResolver.SCHEME_FILE.equals(mUri.getScheme());
292    }
293
294    @Override
295    public int getMediaType() {
296        return MEDIA_TYPE_IMAGE;
297    }
298
299    @Override
300    public Uri getContentUri() {
301        return mUri;
302    }
303
304    @Override
305    public MediaDetails getDetails() {
306        MediaDetails details = super.getDetails();
307        if (mWidth != 0 && mHeight != 0) {
308            details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
309            details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
310        }
311        if (mContentType != null) {
312            details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
313        }
314        if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
315            String filePath = mUri.getPath();
316            details.addDetail(MediaDetails.INDEX_PATH, filePath);
317            MediaDetails.extractExifInfo(details, filePath);
318        }
319        return details;
320    }
321
322    @Override
323    public String getMimeType() {
324        return mContentType;
325    }
326
327    @Override
328    protected void finalize() throws Throwable {
329        try {
330            if (mFileDescriptor != null) {
331                Utils.closeSilently(mFileDescriptor);
332            }
333        } finally {
334            super.finalize();
335        }
336    }
337
338    @Override
339    public int getWidth() {
340        return 0;
341    }
342
343    @Override
344    public int getHeight() {
345        return 0;
346    }
347
348    @Override
349    public int getRotation() {
350        return mRotation;
351    }
352}
353