UriImage.java revision 4bb5912e85f6d1bd8a6b78d6d52b4c4da7aeb740
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.graphics.Bitmap;
21import android.graphics.Bitmap.Config;
22import android.graphics.BitmapFactory.Options;
23import android.graphics.BitmapRegionDecoder;
24import android.net.Uri;
25import android.os.ParcelFileDescriptor;
26import android.webkit.MimeTypeMap;
27
28import com.android.gallery3d.app.GalleryApp;
29import com.android.gallery3d.common.BitmapUtils;
30import com.android.gallery3d.common.Utils;
31import com.android.gallery3d.util.ThreadPool.CancelListener;
32import com.android.gallery3d.util.ThreadPool.Job;
33import com.android.gallery3d.util.ThreadPool.JobContext;
34
35import java.io.FileInputStream;
36import java.io.FileNotFoundException;
37import java.io.InputStream;
38import java.net.URI;
39import java.net.URL;
40
41public class UriImage extends MediaItem {
42    private static final String TAG = "UriImage";
43
44    private static final int STATE_INIT = 0;
45    private static final int STATE_DOWNLOADING = 1;
46    private static final int STATE_DOWNLOADED = 2;
47    private static final int STATE_ERROR = -1;
48
49    private final Uri mUri;
50    private final String mContentType;
51
52    private DownloadCache.Entry mCacheEntry;
53    private ParcelFileDescriptor mFileDescriptor;
54    private int mState = STATE_INIT;
55    private int mWidth;
56    private int mHeight;
57    private int mRotation;
58
59    private GalleryApp mApplication;
60
61    public UriImage(GalleryApp application, Path path, Uri uri) {
62        super(path, nextVersionNumber());
63        mUri = uri;
64        mApplication = Utils.checkNotNull(application);
65        mContentType = getMimeType(uri);
66    }
67
68    private String getMimeType(Uri uri) {
69        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
70            String extension =
71                    MimeTypeMap.getFileExtensionFromUrl(uri.toString());
72            String type = MimeTypeMap.getSingleton()
73                    .getMimeTypeFromExtension(extension.toLowerCase());
74            if (type != null) return type;
75        }
76        return mApplication.getContentResolver().getType(uri);
77    }
78
79    @Override
80    public Job<Bitmap> requestImage(int type) {
81        return new BitmapJob(type);
82    }
83
84    @Override
85    public Job<BitmapRegionDecoder> requestLargeImage() {
86        return new RegionDecoderJob();
87    }
88
89    private void openFileOrDownloadTempFile(JobContext jc) {
90        int state = openOrDownloadInner(jc);
91        synchronized (this) {
92            mState = state;
93            if (mState != STATE_DOWNLOADED) {
94                if (mFileDescriptor != null) {
95                    Utils.closeSilently(mFileDescriptor);
96                    mFileDescriptor = null;
97                }
98            }
99            notifyAll();
100        }
101    }
102
103    private int openOrDownloadInner(JobContext jc) {
104        String scheme = mUri.getScheme();
105        if (ContentResolver.SCHEME_CONTENT.equals(scheme)
106                || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
107                || ContentResolver.SCHEME_FILE.equals(scheme)) {
108            try {
109                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
110                    InputStream is = mApplication.getContentResolver()
111                            .openInputStream(mUri);
112                    mRotation = Exif.getOrientation(is);
113                    Utils.closeSilently(is);
114                }
115                mFileDescriptor = mApplication.getContentResolver()
116                        .openFileDescriptor(mUri, "r");
117                if (jc.isCancelled()) return STATE_INIT;
118                return STATE_DOWNLOADED;
119            } catch (FileNotFoundException e) {
120                Log.w(TAG, "fail to open: " + mUri, e);
121                return STATE_ERROR;
122            }
123        } else {
124            try {
125                URL url = new URI(mUri.toString()).toURL();
126                mCacheEntry = mApplication.getDownloadCache().download(jc, url);
127                if (jc.isCancelled()) return STATE_INIT;
128                if (mCacheEntry == null) {
129                    Log.w(TAG, "download failed " + url);
130                    return STATE_ERROR;
131                }
132                if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
133                    InputStream is = new FileInputStream(mCacheEntry.cacheFile);
134                    mRotation = Exif.getOrientation(is);
135                    Utils.closeSilently(is);
136                }
137                mFileDescriptor = ParcelFileDescriptor.open(
138                        mCacheEntry.cacheFile, ParcelFileDescriptor.MODE_READ_ONLY);
139                return STATE_DOWNLOADED;
140            } catch (Throwable t) {
141                Log.w(TAG, "download error", t);
142                return STATE_ERROR;
143            }
144        }
145    }
146
147    private boolean prepareInputFile(JobContext jc) {
148        jc.setCancelListener(new CancelListener() {
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        public BitmapRegionDecoder run(JobContext jc) {
182            if (!prepareInputFile(jc)) return null;
183            BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
184                    jc, mFileDescriptor.getFileDescriptor(), false);
185            mWidth = decoder.getWidth();
186            mHeight = decoder.getHeight();
187            return decoder;
188        }
189    }
190
191    private class BitmapJob implements Job<Bitmap> {
192        private int mType;
193
194        protected BitmapJob(int type) {
195            mType = type;
196        }
197
198        @Override
199        public Bitmap run(JobContext jc) {
200            if (!prepareInputFile(jc)) return null;
201            int targetSize = MediaItem.getTargetSize(mType);
202            Options options = new Options();
203            options.inPreferredConfig = Config.ARGB_8888;
204            Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
205                    mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
206
207            if (jc.isCancelled() || bitmap == null) {
208                return null;
209            }
210
211            if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
212                bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
213            } else {
214                bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
215            }
216            return bitmap;
217        }
218    }
219
220    @Override
221    public int getSupportedOperations() {
222        int supported = SUPPORT_EDIT | SUPPORT_SETAS;
223        if (isSharable()) supported |= SUPPORT_SHARE;
224        if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
225            supported |= SUPPORT_FULL_IMAGE;
226        }
227        return supported;
228    }
229
230    private boolean isSharable() {
231        // We cannot grant read permission to the receiver since we put
232        // the data URI in EXTRA_STREAM instead of the data part of an intent
233        // And there are issues in MediaUploader and Bluetooth file sender to
234        // share a general image data. So, we only share for local file.
235        return ContentResolver.SCHEME_FILE.equals(mUri.getScheme());
236    }
237
238    @Override
239    public int getMediaType() {
240        return MEDIA_TYPE_IMAGE;
241    }
242
243    @Override
244    public Uri getContentUri() {
245        return mUri;
246    }
247
248    @Override
249    public MediaDetails getDetails() {
250        MediaDetails details = super.getDetails();
251        if (mWidth != 0 && mHeight != 0) {
252            details.addDetail(MediaDetails.INDEX_WIDTH, mWidth);
253            details.addDetail(MediaDetails.INDEX_HEIGHT, mHeight);
254        }
255        if (mContentType != null) {
256            details.addDetail(MediaDetails.INDEX_MIMETYPE, mContentType);
257        }
258        if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) {
259            String filePath = mUri.getPath();
260            details.addDetail(MediaDetails.INDEX_PATH, filePath);
261            MediaDetails.extractExifInfo(details, filePath);
262        }
263        return details;
264    }
265
266    @Override
267    public String getMimeType() {
268        return mContentType;
269    }
270
271    @Override
272    protected void finalize() throws Throwable {
273        try {
274            if (mFileDescriptor != null) {
275                Utils.closeSilently(mFileDescriptor);
276            }
277        } finally {
278            super.finalize();
279        }
280    }
281
282    @Override
283    public int getWidth() {
284        return 0;
285    }
286
287    @Override
288    public int getHeight() {
289        return 0;
290    }
291
292    @Override
293    public int getRotation() {
294        return mRotation;
295    }
296}
297