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