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