Util.java revision 35627500f00644aaae44c93f98f6a0cdeefe2d94
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 synchronized OnClickListener getNullOnClickListener() {
365        if (sNullOnClickListener == null) {
366            sNullOnClickListener = new OnClickListener() {
367                public void onClick(View v) {
368                }
369            };
370        }
371        return sNullOnClickListener;
372    }
373
374    public static void Assert(boolean cond) {
375        if (!cond) {
376            throw new AssertionError();
377        }
378    }
379
380    public static boolean equals(String a, String b) {
381        // return true if both string are null or the content equals
382        return a == b || a.equals(b);
383    }
384
385    private static class BackgroundJob
386            extends MonitoredActivity.LifeCycleAdapter implements Runnable {
387
388        private final MonitoredActivity mActivity;
389        private final ProgressDialog mDialog;
390        private final Runnable mJob;
391        private final Handler mHandler;
392        private final Runnable mCleanupRunner = new Runnable() {
393            public void run() {
394                mActivity.removeLifeCycleListener(BackgroundJob.this);
395                if (mDialog.getWindow() != null) mDialog.dismiss();
396            }
397        };
398
399        public BackgroundJob(MonitoredActivity activity, Runnable job,
400                ProgressDialog dialog, Handler handler) {
401            mActivity = activity;
402            mDialog = dialog;
403            mJob = job;
404            mActivity.addLifeCycleListener(this);
405            mHandler = handler;
406        }
407
408        public void run() {
409            try {
410                mJob.run();
411            } finally {
412                mHandler.post(mCleanupRunner);
413            }
414        }
415
416
417        @Override
418        public void onActivityDestroyed(MonitoredActivity activity) {
419            // We get here only when the onDestroyed being called before
420            // the mCleanupRunner. So, run it now and remove it from the queue
421            mCleanupRunner.run();
422            mHandler.removeCallbacks(mCleanupRunner);
423        }
424
425        @Override
426        public void onActivityStopped(MonitoredActivity activity) {
427            mDialog.hide();
428        }
429
430        @Override
431        public void onActivityStarted(MonitoredActivity activity) {
432            mDialog.show();
433        }
434    }
435
436    public static void startBackgroundJob(MonitoredActivity activity,
437            String title, String message, Runnable job, Handler handler) {
438        // Make the progress dialog uncancelable, so that we can gurantee
439        // the thread will be done before the activity getting destroyed.
440        ProgressDialog dialog = ProgressDialog.show(
441                activity, title, message, true, false);
442        new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
443    }
444
445    // Returns an intent which is used for "set as" menu items.
446    public static Intent createSetAsIntent(IImage image) {
447        Uri u = image.fullSizeImageUri();
448        Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
449        intent.setDataAndType(u, image.getMimeType());
450        intent.putExtra("mimeType", image.getMimeType());
451        return intent;
452    }
453
454    // Returns Options that set the puregeable flag for Bitmap decode.
455    public static BitmapFactory.Options createNativeAllocOptions() {
456        BitmapFactory.Options options = new BitmapFactory.Options();
457        options.inNativeAlloc = true;
458        return options;
459    }
460
461    public static void showFatalErrorAndFinish(
462            final Activity activity, String title, String message) {
463        DialogInterface.OnClickListener buttonListener =
464                new DialogInterface.OnClickListener() {
465            public void onClick(DialogInterface dialog, int which) {
466                activity.finish();
467            }
468        };
469        new AlertDialog.Builder(activity)
470                .setCancelable(false)
471                .setIcon(android.R.drawable.ic_dialog_alert)
472                .setTitle(title)
473                .setMessage(message)
474                .setNeutralButton(R.string.details_ok, buttonListener)
475                .show();
476    }
477
478    public static Animation slideOut(View view, int to) {
479        view.setVisibility(View.INVISIBLE);
480        Animation anim;
481        switch (to) {
482            case DIRECTION_LEFT:
483                anim = new TranslateAnimation(0, -view.getWidth(), 0, 0);
484                break;
485            case DIRECTION_RIGHT:
486                anim = new TranslateAnimation(0, view.getWidth(), 0, 0);
487                break;
488            case DIRECTION_UP:
489                anim = new TranslateAnimation(0, 0, 0, -view.getHeight());
490                break;
491            case DIRECTION_DOWN:
492                anim = new TranslateAnimation(0, 0, 0, view.getHeight());
493                break;
494            default:
495                throw new IllegalArgumentException(Integer.toString(to));
496        }
497        anim.setDuration(500);
498        view.startAnimation(anim);
499        return anim;
500    }
501
502    public static Animation slideIn(View view, int from) {
503        view.setVisibility(View.VISIBLE);
504        Animation anim;
505        switch (from) {
506            case DIRECTION_LEFT:
507                anim = new TranslateAnimation(-view.getWidth(), 0, 0, 0);
508                break;
509            case DIRECTION_RIGHT:
510                anim = new TranslateAnimation(view.getWidth(), 0, 0, 0);
511                break;
512            case DIRECTION_UP:
513                anim = new TranslateAnimation(0, 0, -view.getHeight(), 0);
514                break;
515            case DIRECTION_DOWN:
516                anim = new TranslateAnimation(0, 0, view.getHeight(), 0);
517                break;
518            default:
519                throw new IllegalArgumentException(Integer.toString(from));
520        }
521        anim.setDuration(500);
522        view.startAnimation(anim);
523        return anim;
524    }
525}
526