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