Util.java revision 34fe2a9d33a5b4ce49862ce4558a36c62ba6d540
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 = "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 // Whether we should recycle the input (unless the output is the input). 138 public static boolean RECYCLE_INPUT = true; 139 public static boolean NO_RECYCLE_INPUT = false; 140 141 public static Bitmap transform(Matrix scaler, 142 Bitmap source, 143 int targetWidth, 144 int targetHeight, 145 boolean scaleUp, 146 boolean recycle) { 147 int deltaX = source.getWidth() - targetWidth; 148 int deltaY = source.getHeight() - targetHeight; 149 if (!scaleUp && (deltaX < 0 || deltaY < 0)) { 150 /* 151 * In this case the bitmap is smaller, at least in one dimension, 152 * than the target. Transform it by placing as much of the image 153 * as possible into the target and leaving the top/bottom or 154 * left/right (or both) black. 155 */ 156 Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, 157 Bitmap.Config.ARGB_8888); 158 Canvas c = new Canvas(b2); 159 160 int deltaXHalf = Math.max(0, deltaX / 2); 161 int deltaYHalf = Math.max(0, deltaY / 2); 162 Rect src = new Rect( 163 deltaXHalf, 164 deltaYHalf, 165 deltaXHalf + Math.min(targetWidth, source.getWidth()), 166 deltaYHalf + Math.min(targetHeight, source.getHeight())); 167 int dstX = (targetWidth - src.width()) / 2; 168 int dstY = (targetHeight - src.height()) / 2; 169 Rect dst = new Rect( 170 dstX, 171 dstY, 172 targetWidth - dstX, 173 targetHeight - dstY); 174 c.drawBitmap(source, src, dst, null); 175 if (recycle) { 176 source.recycle(); 177 } 178 return b2; 179 } 180 float bitmapWidthF = source.getWidth(); 181 float bitmapHeightF = source.getHeight(); 182 183 float bitmapAspect = bitmapWidthF / bitmapHeightF; 184 float viewAspect = (float) targetWidth / targetHeight; 185 186 if (bitmapAspect > viewAspect) { 187 float scale = targetHeight / bitmapHeightF; 188 if (scale < .9F || scale > 1F) { 189 scaler.setScale(scale, scale); 190 } else { 191 scaler = null; 192 } 193 } else { 194 float scale = targetWidth / bitmapWidthF; 195 if (scale < .9F || scale > 1F) { 196 scaler.setScale(scale, scale); 197 } else { 198 scaler = null; 199 } 200 } 201 202 Bitmap b1; 203 if (scaler != null) { 204 // this is used for minithumb and crop, so we want to filter here. 205 b1 = Bitmap.createBitmap(source, 0, 0, 206 source.getWidth(), source.getHeight(), scaler, true); 207 } else { 208 b1 = source; 209 } 210 211 if (recycle && b1 != source) { 212 source.recycle(); 213 } 214 215 int dx1 = Math.max(0, b1.getWidth() - targetWidth); 216 int dy1 = Math.max(0, b1.getHeight() - targetHeight); 217 218 Bitmap b2 = Bitmap.createBitmap( 219 b1, 220 dx1 / 2, 221 dy1 / 2, 222 targetWidth, 223 targetHeight); 224 225 if (b2 != b1) { 226 if (recycle || b1 != source) { 227 b1.recycle(); 228 } 229 } 230 231 return b2; 232 } 233 234 /** 235 * Creates a centered bitmap of the desired size. 236 * @param source 237 * @param recycle whether we want to recycle the input 238 */ 239 public static Bitmap extractMiniThumb( 240 Bitmap source, int width, int height, boolean recycle) { 241 if (source == null) { 242 return null; 243 } 244 245 float scale; 246 if (source.getWidth() < source.getHeight()) { 247 scale = width / (float) source.getWidth(); 248 } else { 249 scale = height / (float) source.getHeight(); 250 } 251 Matrix matrix = new Matrix(); 252 matrix.setScale(scale, scale); 253 Bitmap miniThumbnail = transform(matrix, source, width, height, false, recycle); 254 return miniThumbnail; 255 } 256 257 /** 258 * Creates a byte[] for a given bitmap of the desired size. Recycles the 259 * input bitmap. 260 */ 261 public static byte[] miniThumbData(Bitmap source) { 262 if (source == null) return null; 263 264 Bitmap miniThumbnail = extractMiniThumb( 265 source, IImage.MINI_THUMB_TARGET_SIZE, 266 IImage.MINI_THUMB_TARGET_SIZE, 267 Util.RECYCLE_INPUT); 268 269 ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); 270 miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); 271 miniThumbnail.recycle(); 272 273 try { 274 miniOutStream.close(); 275 byte [] data = miniOutStream.toByteArray(); 276 return data; 277 } catch (java.io.IOException ex) { 278 Log.e(TAG, "got exception ex " + ex); 279 } 280 return null; 281 } 282 283 /** 284 * Create a video thumbnail for a video. May return null if the video is 285 * corrupt. 286 * 287 * @param filePath 288 */ 289 public static Bitmap createVideoThumbnail(String filePath) { 290 Bitmap bitmap = null; 291 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 292 try { 293 retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY); 294 retriever.setDataSource(filePath); 295 bitmap = retriever.captureFrame(); 296 } catch (IllegalArgumentException ex) { 297 // Assume this is a corrupt video file 298 } catch (RuntimeException ex) { 299 // Assume this is a corrupt video file. 300 } finally { 301 try { 302 retriever.release(); 303 } catch (RuntimeException ex) { 304 // Ignore failures while cleaning up. 305 } 306 } 307 return bitmap; 308 } 309 310 public static <T> int indexOf(T [] array, T s) { 311 for (int i = 0; i < array.length; i++) { 312 if (array[i].equals(s)) { 313 return i; 314 } 315 } 316 return -1; 317 } 318 319 public static void closeSilently(Closeable c) { 320 if (c == null) return; 321 try { 322 c.close(); 323 } catch (Throwable t) { 324 // do nothing 325 } 326 } 327 328 public static void closeSilently(ParcelFileDescriptor c) { 329 if (c == null) return; 330 try { 331 c.close(); 332 } catch (Throwable t) { 333 // do nothing 334 } 335 } 336 337 /** 338 * Make a bitmap from a given Uri. 339 * 340 * @param uri 341 */ 342 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 343 Uri uri, ContentResolver cr) { 344 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, 345 IImage.NO_NATIVE); 346 } 347 348 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 349 Uri uri, ContentResolver cr, boolean useNative) { 350 ParcelFileDescriptor input = null; 351 try { 352 input = cr.openFileDescriptor(uri, "r"); 353 BitmapFactory.Options options = null; 354 if (useNative) { 355 options = createNativeAllocOptions(); 356 } 357 return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input, 358 options); 359 } catch (IOException ex) { 360 return null; 361 } finally { 362 closeSilently(input); 363 } 364 } 365 366 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 367 ParcelFileDescriptor pfd, boolean useNative) { 368 BitmapFactory.Options options = null; 369 if (useNative) { 370 options = createNativeAllocOptions(); 371 } 372 return makeBitmap(minSideLength, maxNumOfPixels, null, null, pfd, 373 options); 374 } 375 376 public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels, 377 Uri uri, ContentResolver cr, ParcelFileDescriptor pfd, 378 BitmapFactory.Options options) { 379 Bitmap b = null; 380 try { 381 if (pfd == null) pfd = makeInputStream(uri, cr); 382 if (pfd == null) return null; 383 if (options == null) options = new BitmapFactory.Options(); 384 385 FileDescriptor fd = pfd.getFileDescriptor(); 386 options.inSampleSize = 1; 387 options.inJustDecodeBounds = true; 388 BitmapManager.instance().decodeFileDescriptor(fd, options); 389 if (options.mCancel || options.outWidth == -1 390 || options.outHeight == -1) { 391 return null; 392 } 393 options.inSampleSize = computeSampleSize( 394 options, minSideLength, maxNumOfPixels); 395 options.inJustDecodeBounds = false; 396 397 options.inDither = false; 398 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 399 b = BitmapManager.instance().decodeFileDescriptor(fd, options); 400 } catch (OutOfMemoryError ex) { 401 Log.e(TAG, "Got oom exception ", ex); 402 return null; 403 } finally { 404 closeSilently(pfd); 405 } 406 return b; 407 } 408 409 private static ParcelFileDescriptor makeInputStream( 410 Uri uri, ContentResolver cr) { 411 try { 412 return cr.openFileDescriptor(uri, "r"); 413 } catch (IOException ex) { 414 return null; 415 } 416 } 417 418 public static synchronized OnClickListener getNullOnClickListener() { 419 if (sNullOnClickListener == null) { 420 sNullOnClickListener = new OnClickListener() { 421 public void onClick(View v) { 422 } 423 }; 424 } 425 return sNullOnClickListener; 426 } 427 428 public static void Assert(boolean cond) { 429 if (!cond) { 430 throw new AssertionError(); 431 } 432 } 433 434 public static boolean equals(String a, String b) { 435 // return true if both string are null or the content equals 436 return a == b || a.equals(b); 437 } 438 439 private static class BackgroundJob 440 extends MonitoredActivity.LifeCycleAdapter implements Runnable { 441 442 private final MonitoredActivity mActivity; 443 private final ProgressDialog mDialog; 444 private final Runnable mJob; 445 private final Handler mHandler; 446 private final Runnable mCleanupRunner = new Runnable() { 447 public void run() { 448 mActivity.removeLifeCycleListener(BackgroundJob.this); 449 if (mDialog.getWindow() != null) mDialog.dismiss(); 450 } 451 }; 452 453 public BackgroundJob(MonitoredActivity activity, Runnable job, 454 ProgressDialog dialog, Handler handler) { 455 mActivity = activity; 456 mDialog = dialog; 457 mJob = job; 458 mActivity.addLifeCycleListener(this); 459 mHandler = handler; 460 } 461 462 public void run() { 463 try { 464 mJob.run(); 465 } finally { 466 mHandler.post(mCleanupRunner); 467 } 468 } 469 470 471 @Override 472 public void onActivityDestroyed(MonitoredActivity activity) { 473 // We get here only when the onDestroyed being called before 474 // the mCleanupRunner. So, run it now and remove it from the queue 475 mCleanupRunner.run(); 476 mHandler.removeCallbacks(mCleanupRunner); 477 } 478 479 @Override 480 public void onActivityStopped(MonitoredActivity activity) { 481 mDialog.hide(); 482 } 483 484 @Override 485 public void onActivityStarted(MonitoredActivity activity) { 486 mDialog.show(); 487 } 488 } 489 490 public static void startBackgroundJob(MonitoredActivity activity, 491 String title, String message, Runnable job, Handler handler) { 492 // Make the progress dialog uncancelable, so that we can gurantee 493 // the thread will be done before the activity getting destroyed. 494 ProgressDialog dialog = ProgressDialog.show( 495 activity, title, message, true, false); 496 new Thread(new BackgroundJob(activity, job, dialog, handler)).start(); 497 } 498 499 // Returns an intent which is used for "set as" menu items. 500 public static Intent createSetAsIntent(IImage image) { 501 Uri u = image.fullSizeImageUri(); 502 Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); 503 intent.setDataAndType(u, image.getMimeType()); 504 intent.putExtra("mimeType", image.getMimeType()); 505 return intent; 506 } 507 508 // Returns Options that set the puregeable flag for Bitmap decode. 509 public static BitmapFactory.Options createNativeAllocOptions() { 510 BitmapFactory.Options options = new BitmapFactory.Options(); 511 options.inNativeAlloc = true; 512 return options; 513 } 514} 515