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