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