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