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