ImageLoader.java revision 8965d1f4c2d437e0a0ad4fd225ea2cad9d2471c4
1/* 2 * Copyright (C) 2012 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.filtershow.cache; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.res.Resources; 22import android.database.Cursor; 23import android.database.sqlite.SQLiteException; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.graphics.BitmapRegionDecoder; 27import android.graphics.Matrix; 28import android.graphics.Rect; 29import android.media.ExifInterface; 30import android.net.Uri; 31import android.provider.MediaStore; 32import android.util.Log; 33 34import com.adobe.xmp.XMPException; 35import com.adobe.xmp.XMPMeta; 36 37import com.android.gallery3d.R; 38import com.android.gallery3d.common.Utils; 39import com.android.gallery3d.exif.ExifInvalidFormatException; 40import com.android.gallery3d.exif.ExifParser; 41import com.android.gallery3d.exif.ExifTag; 42import com.android.gallery3d.filtershow.FilterShowActivity; 43import com.android.gallery3d.filtershow.HistoryAdapter; 44import com.android.gallery3d.filtershow.imageshow.ImageCrop; 45import com.android.gallery3d.filtershow.imageshow.ImageShow; 46import com.android.gallery3d.filtershow.presets.ImagePreset; 47import com.android.gallery3d.filtershow.tools.SaveCopyTask; 48import com.android.gallery3d.util.XmpUtilHelper; 49 50import java.io.Closeable; 51import java.io.File; 52import java.io.FileInputStream; 53import java.io.FileNotFoundException; 54import java.io.IOException; 55import java.io.InputStream; 56import java.util.Vector; 57import java.util.concurrent.locks.ReentrantLock; 58 59public class ImageLoader { 60 61 private static final String LOGTAG = "ImageLoader"; 62 private final Vector<ImageShow> mListeners = new Vector<ImageShow>(); 63 private Bitmap mOriginalBitmapSmall = null; 64 private Bitmap mOriginalBitmapLarge = null; 65 private Bitmap mBackgroundBitmap = null; 66 67 private Cache mCache = null; 68 private Cache mHiresCache = null; 69 private final ZoomCache mZoomCache = new ZoomCache(); 70 71 private int mOrientation = 0; 72 private HistoryAdapter mAdapter = null; 73 74 private FilterShowActivity mActivity = null; 75 76 public static final int ORI_NORMAL = ExifInterface.ORIENTATION_NORMAL; 77 public static final int ORI_ROTATE_90 = ExifInterface.ORIENTATION_ROTATE_90; 78 public static final int ORI_ROTATE_180 = ExifInterface.ORIENTATION_ROTATE_180; 79 public static final int ORI_ROTATE_270 = ExifInterface.ORIENTATION_ROTATE_270; 80 public static final int ORI_FLIP_HOR = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; 81 public static final int ORI_FLIP_VERT = ExifInterface.ORIENTATION_FLIP_VERTICAL; 82 public static final int ORI_TRANSPOSE = ExifInterface.ORIENTATION_TRANSPOSE; 83 public static final int ORI_TRANSVERSE = ExifInterface.ORIENTATION_TRANSVERSE; 84 85 private Context mContext = null; 86 private Uri mUri = null; 87 88 private Rect mOriginalBounds = null; 89 private static int mZoomOrientation = ORI_NORMAL; 90 91 private ReentrantLock mLoadingLock = new ReentrantLock(); 92 93 public ImageLoader(FilterShowActivity activity, Context context) { 94 mActivity = activity; 95 mContext = context; 96 mCache = new DelayedPresetCache(this, 30); 97 mHiresCache = new DelayedPresetCache(this, 3); 98 } 99 100 public static int getZoomOrientation() { 101 return mZoomOrientation; 102 } 103 104 public FilterShowActivity getActivity() { 105 return mActivity; 106 } 107 108 public boolean loadBitmap(Uri uri, int size) { 109 mLoadingLock.lock(); 110 mUri = uri; 111 mOrientation = getOrientation(mContext, uri); 112 mOriginalBitmapSmall = loadScaledBitmap(uri, 160); 113 if (mOriginalBitmapSmall == null) { 114 // Couldn't read the bitmap, let's exit 115 mLoadingLock.unlock(); 116 return false; 117 } 118 mOriginalBitmapLarge = loadScaledBitmap(uri, size); 119 if (mOriginalBitmapLarge == null) { 120 mLoadingLock.unlock(); 121 return false; 122 } 123 updateBitmaps(); 124 mLoadingLock.unlock(); 125 return true; 126 } 127 128 public Uri getUri() { 129 return mUri; 130 } 131 132 public Rect getOriginalBounds() { 133 return mOriginalBounds; 134 } 135 136 public static int getOrientation(Context context, Uri uri) { 137 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { 138 return getOrientationFromPath(uri.getPath()); 139 } 140 141 Cursor cursor = null; 142 try { 143 cursor = context.getContentResolver().query(uri, 144 new String[] { 145 MediaStore.Images.ImageColumns.ORIENTATION 146 }, 147 null, null, null); 148 if (cursor.moveToNext()) { 149 int ori = cursor.getInt(0); 150 151 switch (ori) { 152 case 0: 153 return ORI_NORMAL; 154 case 90: 155 return ORI_ROTATE_90; 156 case 270: 157 return ORI_ROTATE_270; 158 case 180: 159 return ORI_ROTATE_180; 160 default: 161 return -1; 162 } 163 } else { 164 return -1; 165 } 166 } catch (SQLiteException e) { 167 return ExifInterface.ORIENTATION_UNDEFINED; 168 } finally { 169 Utils.closeSilently(cursor); 170 } 171 } 172 173 static int getOrientationFromPath(String path) { 174 int orientation = -1; 175 InputStream is = null; 176 try { 177 is = new FileInputStream(path); 178 ExifParser parser = ExifParser.parse(is, ExifParser.OPTION_IFD_0); 179 int event = parser.next(); 180 while (event != ExifParser.EVENT_END) { 181 if (event == ExifParser.EVENT_NEW_TAG) { 182 ExifTag tag = parser.getTag(); 183 if (tag.getTagId() == ExifTag.TAG_ORIENTATION) { 184 orientation = (int) tag.getValueAt(0); 185 break; 186 } 187 } 188 event = parser.next(); 189 } 190 } catch (IOException e) { 191 e.printStackTrace(); 192 } catch (ExifInvalidFormatException e) { 193 e.printStackTrace(); 194 } finally { 195 Utils.closeSilently(is); 196 } 197 return orientation; 198 } 199 200 private void updateBitmaps() { 201 if (mOrientation > 1) { 202 mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation); 203 mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation); 204 } 205 mZoomOrientation = mOrientation; 206 mCache.setOriginalBitmap(mOriginalBitmapSmall); 207 mHiresCache.setOriginalBitmap(mOriginalBitmapLarge); 208 warnListeners(); 209 } 210 211 public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) { 212 Matrix matrix = new Matrix(); 213 int w = bitmap.getWidth(); 214 int h = bitmap.getHeight(); 215 if (ori == ORI_ROTATE_90 || 216 ori == ORI_ROTATE_270 || 217 ori == ORI_TRANSPOSE || 218 ori == ORI_TRANSVERSE) { 219 int tmp = w; 220 w = h; 221 h = tmp; 222 } 223 switch (ori) { 224 case ORI_ROTATE_90: 225 matrix.setRotate(90, w / 2f, h / 2f); 226 break; 227 case ORI_ROTATE_180: 228 matrix.setRotate(180, w / 2f, h / 2f); 229 break; 230 case ORI_ROTATE_270: 231 matrix.setRotate(270, w / 2f, h / 2f); 232 break; 233 case ORI_FLIP_HOR: 234 matrix.preScale(-1, 1); 235 break; 236 case ORI_FLIP_VERT: 237 matrix.preScale(1, -1); 238 break; 239 case ORI_TRANSPOSE: 240 matrix.setRotate(90, w / 2f, h / 2f); 241 matrix.preScale(1, -1); 242 break; 243 case ORI_TRANSVERSE: 244 matrix.setRotate(270, w / 2f, h / 2f); 245 matrix.preScale(1, -1); 246 break; 247 case ORI_NORMAL: 248 default: 249 return bitmap; 250 } 251 252 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 253 bitmap.getHeight(), matrix, true); 254 } 255 256 private void closeStream(Closeable stream) { 257 if (stream != null) { 258 try { 259 stream.close(); 260 } catch (IOException e) { 261 e.printStackTrace(); 262 } 263 } 264 } 265 266 private Bitmap loadRegionBitmap(Uri uri, Rect bounds) { 267 InputStream is = null; 268 try { 269 is = mContext.getContentResolver().openInputStream(uri); 270 BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 271 return decoder.decodeRegion(bounds, null); 272 } catch (FileNotFoundException e) { 273 Log.e(LOGTAG, "FileNotFoundException: " + uri); 274 } catch (Exception e) { 275 e.printStackTrace(); 276 } finally { 277 closeStream(is); 278 } 279 return null; 280 } 281 282 static final int MAX_BITMAP_DIM = 2048; 283 284 private Bitmap loadScaledBitmap(Uri uri, int size) { 285 InputStream is = null; 286 try { 287 is = mContext.getContentResolver().openInputStream(uri); 288 Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: " 289 + is); 290 BitmapFactory.Options o = new BitmapFactory.Options(); 291 o.inJustDecodeBounds = true; 292 BitmapFactory.decodeStream(is, null, o); 293 294 int width_tmp = o.outWidth; 295 int height_tmp = o.outHeight; 296 297 mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp); 298 299 int scale = 1; 300 while (true) { 301 if (width_tmp <= MAX_BITMAP_DIM && height_tmp <= MAX_BITMAP_DIM) { 302 if (width_tmp / 2 < size || height_tmp / 2 < size) { 303 break; 304 } 305 } 306 width_tmp /= 2; 307 height_tmp /= 2; 308 scale *= 2; 309 } 310 311 // decode with inSampleSize 312 BitmapFactory.Options o2 = new BitmapFactory.Options(); 313 o2.inSampleSize = scale; 314 315 closeStream(is); 316 is = mContext.getContentResolver().openInputStream(uri); 317 return BitmapFactory.decodeStream(is, null, o2); 318 } catch (FileNotFoundException e) { 319 Log.e(LOGTAG, "FileNotFoundException: " + uri); 320 } catch (Exception e) { 321 e.printStackTrace(); 322 } finally { 323 closeStream(is); 324 } 325 return null; 326 } 327 328 public Bitmap getBackgroundBitmap(Resources resources) { 329 if (mBackgroundBitmap == null) { 330 mBackgroundBitmap = BitmapFactory.decodeResource(resources, 331 R.drawable.filtershow_background); 332 } 333 return mBackgroundBitmap; 334 335 } 336 337 public Bitmap getOriginalBitmapSmall() { 338 return mOriginalBitmapSmall; 339 } 340 341 public Bitmap getOriginalBitmapLarge() { 342 return mOriginalBitmapLarge; 343 } 344 345 public void addListener(ImageShow imageShow) { 346 mLoadingLock.lock(); 347 if (!mListeners.contains(imageShow)) { 348 mListeners.add(imageShow); 349 } 350 mHiresCache.addObserver(imageShow); 351 mLoadingLock.unlock(); 352 } 353 354 private void warnListeners() { 355 mActivity.runOnUiThread(mWarnListenersRunnable); 356 } 357 358 private Runnable mWarnListenersRunnable = new Runnable() { 359 360 @Override 361 public void run() { 362 for (int i = 0; i < mListeners.size(); i++) { 363 ImageShow imageShow = mListeners.elementAt(i); 364 imageShow.imageLoaded(); 365 } 366 } 367 }; 368 369 // TODO: this currently does the loading + filtering on the UI thread -- 370 // need to 371 // move this to a background thread. 372 public Bitmap getScaleOneImageForPreset(ImageShow caller, ImagePreset imagePreset, Rect bounds, 373 boolean force) { 374 mLoadingLock.lock(); 375 Bitmap bmp = mZoomCache.getImage(imagePreset, bounds); 376 if (force || bmp == null) { 377 bmp = loadRegionBitmap(mUri, bounds); 378 if (bmp != null) { 379 // TODO: this workaround for RS might not be needed ultimately 380 Bitmap bmp2 = bmp.copy(Bitmap.Config.ARGB_8888, true); 381 float scaleFactor = imagePreset.getScaleFactor(); 382 imagePreset.setScaleFactor(1.0f); 383 bmp2 = imagePreset.apply(bmp2); 384 imagePreset.setScaleFactor(scaleFactor); 385 mZoomCache.setImage(imagePreset, bounds, bmp2); 386 mLoadingLock.unlock(); 387 return bmp2; 388 } 389 } 390 mLoadingLock.unlock(); 391 return bmp; 392 } 393 394 // Caching method 395 public Bitmap getImageForPreset(ImageShow caller, ImagePreset imagePreset, 396 boolean hiRes) { 397 mLoadingLock.lock(); 398 if (mOriginalBitmapSmall == null) { 399 mLoadingLock.unlock(); 400 return null; 401 } 402 if (mOriginalBitmapLarge == null) { 403 mLoadingLock.unlock(); 404 return null; 405 } 406 407 Bitmap filteredImage = null; 408 409 if (hiRes) { 410 filteredImage = mHiresCache.get(imagePreset); 411 } else { 412 filteredImage = mCache.get(imagePreset); 413 } 414 415 if (filteredImage == null) { 416 if (hiRes) { 417 mHiresCache.prepare(imagePreset); 418 mHiresCache.addObserver(caller); 419 } else { 420 mCache.prepare(imagePreset); 421 mCache.addObserver(caller); 422 } 423 } 424 mLoadingLock.unlock(); 425 return filteredImage; 426 } 427 428 public void resetImageForPreset(ImagePreset imagePreset, ImageShow caller) { 429 mLoadingLock.lock(); 430 mHiresCache.reset(imagePreset); 431 mCache.reset(imagePreset); 432 mZoomCache.reset(imagePreset); 433 mLoadingLock.unlock(); 434 } 435 436 public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, 437 File destination) { 438 preset.setIsHighQuality(true); 439 preset.setScaleFactor(1.0f); 440 new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() { 441 442 @Override 443 public void onComplete(Uri result) { 444 filterShowActivity.completeSaveImage(result); 445 } 446 447 }).execute(preset); 448 } 449 450 public void setAdapter(HistoryAdapter adapter) { 451 mAdapter = adapter; 452 } 453 454 public HistoryAdapter getHistory() { 455 return mAdapter; 456 } 457 458 public XMPMeta getXmpObject() { 459 try { 460 InputStream is = mContext.getContentResolver().openInputStream(getUri()); 461 return XmpUtilHelper.extractXMPMeta(is); 462 } catch (FileNotFoundException e) { 463 return null; 464 } 465 } 466 467 /** 468 * Determine if this is a light cycle 360 image 469 * @return true if it is a light Cycle image that is full 360 470 */ 471 public boolean queryLightCycle360() { 472 try { 473 InputStream is = mContext.getContentResolver().openInputStream(getUri()); 474 XMPMeta meta = XmpUtilHelper.extractXMPMeta(is); 475 if (meta == null) { 476 return false; 477 } 478 String name = meta.getPacketHeader(); 479 try { 480 String namespace = "http://ns.google.com/photos/1.0/panorama/"; 481 String cropWidthName = "GPano:CroppedAreaImageWidthPixels"; 482 String fullWidthName = "GPano:FullPanoWidthPixels"; 483 484 if (!meta.doesPropertyExist(namespace, cropWidthName)) { 485 return false; 486 } 487 if (!meta.doesPropertyExist(namespace, fullWidthName)) { 488 return false; 489 } 490 491 Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName); 492 Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName); 493 494 // Definition of a 360: 495 // GFullPanoWidthPixels == CroppedAreaImageWidthPixels 496 if (cropValue != null && fullValue != null) { 497 return cropValue.equals(fullValue); 498 } 499 500 return false; 501 } catch (XMPException e) { 502 return false; 503 } 504 } catch (FileNotFoundException e) { 505 return false; 506 } 507 } 508 509} 510