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