Util.java revision 3c21008c953cf6177f7771c176fa81d336e74ed8
1/* 2 * Copyright (C) 2009 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.camera; 18 19import com.android.camera.gallery.IImage; 20 21import android.app.ProgressDialog; 22import android.content.ContentResolver; 23import android.content.Intent; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.graphics.Canvas; 27import android.graphics.Matrix; 28import android.graphics.Rect; 29import android.media.MediaMetadataRetriever; 30import android.net.Uri; 31import android.os.Handler; 32import android.os.ParcelFileDescriptor; 33import android.util.Log; 34import android.view.View; 35import android.view.View.OnClickListener; 36 37import java.io.ByteArrayOutputStream; 38import java.io.Closeable; 39import java.io.FileDescriptor; 40import java.io.IOException; 41 42/** 43 * Collection of utility functions used in this package. 44 */ 45public class Util { 46 private static final String TAG = "db.Util"; 47 48 private static OnClickListener sNullOnClickListener; 49 50 private Util() { 51 } 52 53 // Rotates the bitmap by the specified degree. 54 // If a new bitmap is created, the original bitmap is recycled. 55 public static Bitmap rotate(Bitmap b, int degrees) { 56 if (degrees != 0 && b != null) { 57 Matrix m = new Matrix(); 58 m.setRotate(degrees, 59 (float) b.getWidth() / 2, (float) b.getHeight() / 2); 60 try { 61 Bitmap b2 = Bitmap.createBitmap( 62 b, 0, 0, b.getWidth(), b.getHeight(), m, true); 63 if (b != b2) { 64 b.recycle(); 65 b = b2; 66 } 67 } catch (OutOfMemoryError ex) { 68 // We have no memory to rotate. Return the original bitmap. 69 } 70 } 71 return b; 72 } 73 74 /* 75 * Compute the sample size as a function of minSideLength 76 * and maxNumOfPixels. 77 * minSideLength is used to specify that minimal width or height of a 78 * bitmap. 79 * maxNumOfPixels is used to specify the maximal size in pixels that is 80 * tolerable in terms of memory usage. 81 * 82 * The function returns a sample size based on the constraints. 83 * Both size and minSideLength can be passed in as IImage.UNCONSTRAINED, 84 * which indicates no care of the corresponding constraint. 85 * The functions prefers returning a sample size that 86 * generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED. 87 * 88 * Also, the function rounds up the sample size to a power of 2 or multiple 89 * of 8 because BitmapFactory only honors sample size this way. 90 * For example, BitmapFactory downsamples an image by 2 even though the 91 * request is 3. So we round up the sample size to avoid OOM. 92 */ 93 public static int computeSampleSize(BitmapFactory.Options options, 94 int minSideLength, int maxNumOfPixels) { 95 int initialSize = computeInitialSampleSize(options, minSideLength, 96 maxNumOfPixels); 97 98 int roundedSize; 99 if (initialSize <= 8 ) { 100 roundedSize = 1; 101 while (roundedSize < initialSize) { 102 roundedSize <<= 1; 103 } 104 } else { 105 roundedSize = (initialSize + 7) / 8 * 8; 106 } 107 108 return roundedSize; 109 } 110 111 private static int computeInitialSampleSize(BitmapFactory.Options options, 112 int minSideLength, int maxNumOfPixels) { 113 double w = options.outWidth; 114 double h = options.outHeight; 115 116 int lowerBound = (maxNumOfPixels == IImage.UNCONSTRAINED) ? 1 : 117 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); 118 int upperBound = (minSideLength == IImage.UNCONSTRAINED) ? 128 : 119 (int) Math.min(Math.floor(w / minSideLength), 120 Math.floor(h / minSideLength)); 121 122 if (upperBound < lowerBound) { 123 // return the larger one when there is no overlapping zone. 124 return lowerBound; 125 } 126 127 if ((maxNumOfPixels == IImage.UNCONSTRAINED) && 128 (minSideLength == IImage.UNCONSTRAINED)) { 129 return 1; 130 } else if (minSideLength == IImage.UNCONSTRAINED) { 131 return lowerBound; 132 } else { 133 return upperBound; 134 } 135 } 136 137 public static Bitmap transform(Matrix scaler, 138 Bitmap source, 139 int targetWidth, 140 int targetHeight, 141 boolean scaleUp) { 142 int deltaX = source.getWidth() - targetWidth; 143 int deltaY = source.getHeight() - targetHeight; 144 if (!scaleUp && (deltaX < 0 || deltaY < 0)) { 145 /* 146 * In this case the bitmap is smaller, at least in one dimension, 147 * than the target. Transform it by placing as much of the image 148 * as possible into the target and leaving the top/bottom or 149 * left/right (or both) black. 150 */ 151 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, 152 Bitmap.Config.ARGB_8888); 153 Canvas c = new Canvas(b2); 154 155 int deltaXHalf = Math.max(0, deltaX / 2); 156 int deltaYHalf = Math.max(0, deltaY / 2); 157 Rect src = new Rect( 158 deltaXHalf, 159 deltaYHalf, 160 deltaXHalf + Math.min(targetWidth, source.getWidth()), 161 deltaYHalf + Math.min(targetHeight, source.getHeight())); 162 int dstX = (targetWidth - src.width()) / 2; 163 int dstY = (targetHeight - src.height()) / 2; 164 Rect dst = new Rect( 165 dstX, 166 dstY, 167 targetWidth - dstX, 168 targetHeight - dstY); 169 c.drawBitmap(source, src, dst, null); 170 return b2; 171 } 172 float bitmapWidthF = source.getWidth(); 173 float bitmapHeightF = source.getHeight(); 174 175 float bitmapAspect = bitmapWidthF / bitmapHeightF; 176 float viewAspect = (float) targetWidth / targetHeight; 177 178 if (bitmapAspect > viewAspect) { 179 float scale = targetHeight / bitmapHeightF; 180 if (scale < .9F || scale > 1F) { 181 scaler.setScale(scale, scale); 182 } else { 183 scaler = null; 184 } 185 } else { 186 float scale = targetWidth / bitmapWidthF; 187 if (scale < .9F || scale > 1F) { 188 scaler.setScale(scale, scale); 189 } else { 190 scaler = null; 191 } 192 } 193 194 Bitmap b1; 195 if (scaler != null) { 196 // this is used for minithumb and crop, so we want to filter here. 197 b1 = Bitmap.createBitmap(source, 0, 0, 198 source.getWidth(), source.getHeight(), scaler, true); 199 } else { 200 b1 = source; 201 } 202 203 int dx1 = Math.max(0, b1.getWidth() - targetWidth); 204 int dy1 = Math.max(0, b1.getHeight() - targetHeight); 205 206 Bitmap b2 = Bitmap.createBitmap( 207 b1, 208 dx1 / 2, 209 dy1 / 2, 210 targetWidth, 211 targetHeight); 212 213 if (b1 != source) { 214 b1.recycle(); 215 } 216 217 return b2; 218 } 219 220 /** 221 * Creates a centered bitmap of the desired size. Recycles the input. 222 * @param source 223 */ 224 public static Bitmap extractMiniThumb( 225 Bitmap source, int width, int height) { 226 return Util.extractMiniThumb(source, width, height, true); 227 } 228 229 public static Bitmap extractMiniThumb( 230 Bitmap source, int width, int height, boolean recycle) { 231 if (source == null) { 232 return null; 233 } 234 235 float scale; 236 if (source.getWidth() < source.getHeight()) { 237 scale = width / (float) source.getWidth(); 238 } else { 239 scale = height / (float) source.getHeight(); 240 } 241 Matrix matrix = new Matrix(); 242 matrix.setScale(scale, scale); 243 Bitmap miniThumbnail = transform(matrix, source, width, height, false); 244 245 if (recycle && miniThumbnail != source) { 246 source.recycle(); 247 } 248 return miniThumbnail; 249 } 250 251 /** 252 * Creates a byte[] for a given bitmap of the desired size. Recycles the 253 * input bitmap. 254 */ 255 public static byte[] miniThumbData(Bitmap source) { 256 if (source == null) return null; 257 258 Bitmap miniThumbnail = extractMiniThumb( 259 source, IImage.MINI_THUMB_TARGET_SIZE, 260 IImage.MINI_THUMB_TARGET_SIZE); 261 262 ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); 263 miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); 264 miniThumbnail.recycle(); 265 266 try { 267 miniOutStream.close(); 268 byte [] data = miniOutStream.toByteArray(); 269 return data; 270 } catch (java.io.IOException ex) { 271 Log.e(TAG, "got exception ex " + ex); 272 } 273 return null; 274 } 275 276 /** 277 * Create a video thumbnail for a video. May return null if the video is 278 * corrupt. 279 * 280 * @param filePath 281 */ 282 public static Bitmap createVideoThumbnail(String filePath) { 283 Bitmap bitmap = null; 284 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 285 try { 286 retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); 287 retriever.setDataSource(filePath); 288 bitmap = retriever.captureFrame(); 289 } catch (IllegalArgumentException ex) { 290 // Assume this is a corrupt video file 291 } catch (RuntimeException ex) { 292 // Assume this is a corrupt video file. 293 } finally { 294 try { 295 retriever.release(); 296 } catch (RuntimeException ex) { 297 // Ignore failures while cleaning up. 298 } 299 } 300 return bitmap; 301 } 302 303 public static <T> int indexOf(T [] array, T s) { 304 for (int i = 0; i < array.length; i++) { 305 if (array[i].equals(s)) { 306 return i; 307 } 308 } 309 return -1; 310 } 311 312 public static void closeSilently(Closeable c) { 313 if (c == null) return; 314 try { 315 c.close(); 316 } catch (Throwable t) { 317 // do nothing 318 } 319 } 320 321 public static void closeSilently(ParcelFileDescriptor c) { 322 if (c == null) return; 323 try { 324 c.close(); 325 } catch (Throwable t) { 326 // do nothing 327 } 328 } 329 330 /** 331 * Make a bitmap from a given Uri. 332 * 333 * @param uri 334 */ 335 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 336 Uri uri, ContentResolver cr) { 337 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, 338 IImage.NO_NATIVE); 339 } 340 341 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 342 Uri uri, ContentResolver cr, boolean useNative) { 343 ParcelFileDescriptor input = null; 344 try { 345 input = cr.openFileDescriptor(uri, "r"); 346 BitmapFactory.Options options = null; 347 if (useNative) { 348 options = createNativeAllocOptions(); 349 } 350 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input, 351 options); 352 } catch (IOException ex) { 353 return null; 354 } finally { 355 closeSilently(input); 356 } 357 } 358 359 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 360 ParcelFileDescriptor pfd, boolean useNative) { 361 BitmapFactory.Options options = null; 362 if (useNative) { 363 options = createNativeAllocOptions(); 364 } 365 return makeBitmap(minSideLength, maxNumOfPixels, null, null, pfd, 366 options); 367 } 368 369 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 370 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, 371 BitmapFactory.Options options) { 372 Bitmap b = null; 373 try { 374 if (pfd == null) pfd = makeInputStream(uri, cr); 375 if (pfd == null) return null; 376 if (options == null) options = new BitmapFactory.Options(); 377 378 FileDescriptor fd = pfd.getFileDescriptor(); 379 options.inSampleSize = 1; 380 options.inJustDecodeBounds = true; 381 BitmapManager.instance().decodeFileDescriptor(fd, options); 382 if (options.mCancel || options.outWidth == -1 383 || options.outHeight == -1) { 384 return null; 385 } 386 options.inSampleSize = computeSampleSize( 387 options, minSideLength, maxNumOfPixels); 388 options.inJustDecodeBounds = false; 389 390 options.inDither = false; 391 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 392 b = BitmapManager.instance().decodeFileDescriptor(fd, options); 393 } catch (OutOfMemoryError ex) { 394 Log.e(TAG, "Got oom exception ", ex); 395 return null; 396 } finally { 397 closeSilently(pfd); 398 } 399 return b; 400 } 401 402 private static ParcelFileDescriptor makeInputStream( 403 Uri uri, ContentResolver cr) { 404 try { 405 return cr.openFileDescriptor(uri, "r"); 406 } catch (IOException ex) { 407 return null; 408 } 409 } 410 411 public static synchronized OnClickListener getNullOnClickListener() { 412 if (sNullOnClickListener == null) { 413 sNullOnClickListener = new OnClickListener() { 414 public void onClick(View v) { 415 } 416 }; 417 } 418 return sNullOnClickListener; 419 } 420 421 public static void Assert(boolean cond) { 422 if (!cond) { 423 throw new AssertionError(); 424 } 425 } 426 427 public static boolean equals(String a, String b) { 428 // return true if both string are null or the content equals 429 return a == b || a.equals(b); 430 } 431 432 private static class BackgroundJob 433 extends MonitoredActivity.LifeCycleAdapter implements Runnable { 434 435 private final MonitoredActivity mActivity; 436 private final ProgressDialog mDialog; 437 private final Runnable mJob; 438 private final Handler mHandler; 439 private final Runnable mCleanupRunner = new Runnable() { 440 public void run() { 441 mActivity.removeLifeCycleListener(BackgroundJob.this); 442 if (mDialog.getWindow() != null) mDialog.dismiss(); 443 } 444 }; 445 446 public BackgroundJob(MonitoredActivity activity, Runnable job, 447 ProgressDialog dialog, Handler handler) { 448 mActivity = activity; 449 mDialog = dialog; 450 mJob = job; 451 mActivity.addLifeCycleListener(this); 452 mHandler = handler; 453 } 454 455 public void run() { 456 try { 457 mJob.run(); 458 } finally { 459 mHandler.post(mCleanupRunner); 460 } 461 } 462 463 464 @Override 465 public void onActivityDestroyed(MonitoredActivity activity) { 466 // We get here only when the onDestroyed being called before 467 // the mCleanupRunner. So, run it now and remove it from the queue 468 mCleanupRunner.run(); 469 mHandler.removeCallbacks(mCleanupRunner); 470 } 471 472 @Override 473 public void onActivityStopped(MonitoredActivity activity) { 474 mDialog.hide(); 475 } 476 477 @Override 478 public void onActivityStarted(MonitoredActivity activity) { 479 mDialog.show(); 480 } 481 } 482 483 public static void startBackgroundJob(MonitoredActivity activity, 484 String title, String message, Runnable job, Handler handler) { 485 // Make the progress dialog uncancelable, so that we can gurantee 486 // the thread will be done before the activity getting destroyed. 487 ProgressDialog dialog = ProgressDialog.show( 488 activity, title, message, true, false); 489 new Thread(new BackgroundJob(activity, job, dialog, handler)).start(); 490 } 491 492 // Returns an intent which is used for "set as" menu items. 493 public static Intent createSetAsIntent(IImage image) { 494 Uri u = image.fullSizeImageUri(); 495 Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); 496 intent.setDataAndType(u, image.getMimeType()); 497 intent.putExtra("mimeType", image.getMimeType()); 498 return intent; 499 } 500 501 // Returns Options that set the puregeable flag for Bitmap decode. 502 public static BitmapFactory.Options createNativeAllocOptions() { 503 BitmapFactory.Options options = new BitmapFactory.Options(); 504 options.inNativeAlloc = true; 505 return options; 506 } 507} 508