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;
26
27import com.android.gallery3d.app.GalleryApp;
28import com.android.gallery3d.app.PanoramaMetadataSupport;
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    private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
59
60    private GalleryApp mApplication;
61
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
70    public Job<Bitmap> requestImage(int type) {
71        return new BitmapJob(type);
72    }
73
74    @Override
75    public Job<BitmapRegionDecoder> requestLargeImage() {
76        return new RegionDecoderJob();
77    }
78
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
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
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
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
186        protected BitmapJob(int type) {
187            mType = type;
188        }
189
190        @Override
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
213    public int getSupportedOperations() {
214        int supported = SUPPORT_EDIT | SUPPORT_SETAS;
215        if (isSharable()) supported |= SUPPORT_SHARE;
216        if (BitmapUtils.isSupportedByRegionDecoder(mContentType)) {
217            supported |= SUPPORT_FULL_IMAGE;
218        }
219        return supported;
220    }
221
222    @Override
223    public void getPanoramaSupport(PanoramaSupportCallback callback) {
224        mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
225    }
226
227    @Override
228    public void clearCachedPanoramaSupport() {
229        mPanoramaMetadata.clearCachedValues();
230    }
231
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
241    public int getMediaType() {
242        return MEDIA_TYPE_IMAGE;
243    }
244
245    @Override
246    public Uri getContentUri() {
247        return mUri;
248    }
249
250    @Override
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
269    public String getMimeType() {
270        return mContentType;
271    }
272
273    @Override
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
285    public int getWidth() {
286        return 0;
287    }
288
289    @Override
290    public int getHeight() {
291        return 0;
292    }
293
294    @Override
295    public int getRotation() {
296        return mRotation;
297    }
298}
299