1/*
2 * Copyright (C) 2010 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.gallery3d.data;
18
19import android.annotation.TargetApi;
20import android.graphics.Bitmap;
21import android.graphics.Bitmap.Config;
22import android.graphics.BitmapFactory;
23import android.graphics.BitmapFactory.Options;
24import android.graphics.BitmapRegionDecoder;
25import android.os.Build;
26import android.util.FloatMath;
27
28import com.android.gallery3d.common.ApiHelper;
29import com.android.gallery3d.common.BitmapUtils;
30import com.android.gallery3d.common.Utils;
31import com.android.photos.data.GalleryBitmapPool;
32import com.android.gallery3d.ui.Log;
33import com.android.gallery3d.util.ThreadPool.CancelListener;
34import com.android.gallery3d.util.ThreadPool.JobContext;
35
36import java.io.FileDescriptor;
37import java.io.FileInputStream;
38import java.io.InputStream;
39
40public class DecodeUtils {
41    private static final String TAG = "DecodeUtils";
42
43    private static class DecodeCanceller implements CancelListener {
44        Options mOptions;
45
46        public DecodeCanceller(Options options) {
47            mOptions = options;
48        }
49
50        @Override
51        public void onCancel() {
52            mOptions.requestCancelDecode();
53        }
54    }
55
56    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
57    public static void setOptionsMutable(Options options) {
58        if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true;
59    }
60
61    public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
62        if (options == null) options = new Options();
63        jc.setCancelListener(new DecodeCanceller(options));
64        setOptionsMutable(options);
65        return ensureGLCompatibleBitmap(
66                BitmapFactory.decodeFileDescriptor(fd, null, options));
67    }
68
69    public static void decodeBounds(JobContext jc, FileDescriptor fd,
70            Options options) {
71        Utils.assertTrue(options != null);
72        options.inJustDecodeBounds = true;
73        jc.setCancelListener(new DecodeCanceller(options));
74        BitmapFactory.decodeFileDescriptor(fd, null, options);
75        options.inJustDecodeBounds = false;
76    }
77
78    public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
79        return decode(jc, bytes, 0, bytes.length, options);
80    }
81
82    public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
83            int length, Options options) {
84        if (options == null) options = new Options();
85        jc.setCancelListener(new DecodeCanceller(options));
86        setOptionsMutable(options);
87        return ensureGLCompatibleBitmap(
88                BitmapFactory.decodeByteArray(bytes, offset, length, options));
89    }
90
91    public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
92            int length, Options options) {
93        Utils.assertTrue(options != null);
94        options.inJustDecodeBounds = true;
95        jc.setCancelListener(new DecodeCanceller(options));
96        BitmapFactory.decodeByteArray(bytes, offset, length, options);
97        options.inJustDecodeBounds = false;
98    }
99
100    public static Bitmap decodeThumbnail(
101            JobContext jc, String filePath, Options options, int targetSize, int type) {
102        FileInputStream fis = null;
103        try {
104            fis = new FileInputStream(filePath);
105            FileDescriptor fd = fis.getFD();
106            return decodeThumbnail(jc, fd, options, targetSize, type);
107        } catch (Exception ex) {
108            Log.w(TAG, ex);
109            return null;
110        } finally {
111            Utils.closeSilently(fis);
112        }
113    }
114
115    public static Bitmap decodeThumbnail(
116            JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
117        if (options == null) options = new Options();
118        jc.setCancelListener(new DecodeCanceller(options));
119
120        options.inJustDecodeBounds = true;
121        BitmapFactory.decodeFileDescriptor(fd, null, options);
122        if (jc.isCancelled()) return null;
123
124        int w = options.outWidth;
125        int h = options.outHeight;
126
127        if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
128            // We center-crop the original image as it's micro thumbnail. In this case,
129            // we want to make sure the shorter side >= "targetSize".
130            float scale = (float) targetSize / Math.min(w, h);
131            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
132
133            // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
134            // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
135            final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
136            if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
137                options.inSampleSize = BitmapUtils.computeSampleSize(
138                        FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h)));
139            }
140        } else {
141            // For screen nail, we only want to keep the longer side >= targetSize.
142            float scale = (float) targetSize / Math.max(w, h);
143            options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
144        }
145
146        options.inJustDecodeBounds = false;
147        setOptionsMutable(options);
148
149        Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
150        if (result == null) return null;
151
152        // We need to resize down if the decoder does not support inSampleSize
153        // (For example, GIF images)
154        float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
155                ? Math.min(result.getWidth(), result.getHeight())
156                : Math.max(result.getWidth(), result.getHeight()));
157
158        if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
159        return ensureGLCompatibleBitmap(result);
160    }
161
162    /**
163     * Decodes the bitmap from the given byte array if the image size is larger than the given
164     * requirement.
165     *
166     * Note: The returned image may be resized down. However, both width and height must be
167     * larger than the <code>targetSize</code>.
168     */
169    public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
170            Options options, int targetSize) {
171        if (options == null) options = new Options();
172        jc.setCancelListener(new DecodeCanceller(options));
173
174        options.inJustDecodeBounds = true;
175        BitmapFactory.decodeByteArray(data, 0, data.length, options);
176        if (jc.isCancelled()) return null;
177        if (options.outWidth < targetSize || options.outHeight < targetSize) {
178            return null;
179        }
180        options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
181                options.outWidth, options.outHeight, targetSize);
182        options.inJustDecodeBounds = false;
183        setOptionsMutable(options);
184
185        return ensureGLCompatibleBitmap(
186                BitmapFactory.decodeByteArray(data, 0, data.length, options));
187    }
188
189    // TODO: This function should not be called directly from
190    // DecodeUtils.requestDecode(...), since we don't have the knowledge
191    // if the bitmap will be uploaded to GL.
192    public static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
193        if (bitmap == null || bitmap.getConfig() != null) return bitmap;
194        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
195        bitmap.recycle();
196        return newBitmap;
197    }
198
199    public static BitmapRegionDecoder createBitmapRegionDecoder(
200            JobContext jc, byte[] bytes, int offset, int length,
201            boolean shareable) {
202        if (offset < 0 || length <= 0 || offset + length > bytes.length) {
203            throw new IllegalArgumentException(String.format(
204                    "offset = %s, length = %s, bytes = %s",
205                    offset, length, bytes.length));
206        }
207
208        try {
209            return BitmapRegionDecoder.newInstance(
210                    bytes, offset, length, shareable);
211        } catch (Throwable t)  {
212            Log.w(TAG, t);
213            return null;
214        }
215    }
216
217    public static BitmapRegionDecoder createBitmapRegionDecoder(
218            JobContext jc, String filePath, boolean shareable) {
219        try {
220            return BitmapRegionDecoder.newInstance(filePath, shareable);
221        } catch (Throwable t)  {
222            Log.w(TAG, t);
223            return null;
224        }
225    }
226
227    public static BitmapRegionDecoder createBitmapRegionDecoder(
228            JobContext jc, FileDescriptor fd, boolean shareable) {
229        try {
230            return BitmapRegionDecoder.newInstance(fd, shareable);
231        } catch (Throwable t)  {
232            Log.w(TAG, t);
233            return null;
234        }
235    }
236
237    public static BitmapRegionDecoder createBitmapRegionDecoder(
238            JobContext jc, InputStream is, boolean shareable) {
239        try {
240            return BitmapRegionDecoder.newInstance(is, shareable);
241        } catch (Throwable t)  {
242            // We often cancel the creating of bitmap region decoder,
243            // so just log one line.
244            Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t);
245            return null;
246        }
247    }
248
249    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
250    public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset,
251            int length, BitmapFactory.Options options) {
252        if (options == null) options = new BitmapFactory.Options();
253        if (options.inSampleSize < 1) options.inSampleSize = 1;
254        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
255        options.inBitmap = (options.inSampleSize == 1)
256                ? findCachedBitmap(jc, data, offset, length, options) : null;
257        try {
258            Bitmap bitmap = decode(jc, data, offset, length, options);
259            if (options.inBitmap != null && options.inBitmap != bitmap) {
260                GalleryBitmapPool.getInstance().put(options.inBitmap);
261                options.inBitmap = null;
262            }
263            return bitmap;
264        } catch (IllegalArgumentException e) {
265            if (options.inBitmap == null) throw e;
266
267            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
268            GalleryBitmapPool.getInstance().put(options.inBitmap);
269            options.inBitmap = null;
270            return decode(jc, data, offset, length, options);
271        }
272    }
273
274    // This is the same as the method above except the source data comes
275    // from a file descriptor instead of a byte array.
276    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
277    public static Bitmap decodeUsingPool(JobContext jc,
278            FileDescriptor fileDescriptor, Options options) {
279        if (options == null) options = new BitmapFactory.Options();
280        if (options.inSampleSize < 1) options.inSampleSize = 1;
281        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
282        options.inBitmap = (options.inSampleSize == 1)
283                ? findCachedBitmap(jc, fileDescriptor, options) : null;
284        try {
285            Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
286            if (options.inBitmap != null && options.inBitmap != bitmap) {
287                GalleryBitmapPool.getInstance().put(options.inBitmap);
288                options.inBitmap = null;
289            }
290            return bitmap;
291        } catch (IllegalArgumentException e) {
292            if (options.inBitmap == null) throw e;
293
294            Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
295            GalleryBitmapPool.getInstance().put(options.inBitmap);
296            options.inBitmap = null;
297            return decode(jc, fileDescriptor, options);
298        }
299    }
300
301    private static Bitmap findCachedBitmap(JobContext jc, byte[] data,
302            int offset, int length, Options options) {
303        decodeBounds(jc, data, offset, length, options);
304        return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
305    }
306
307    private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor,
308            Options options) {
309        decodeBounds(jc, fileDescriptor, options);
310        return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
311    }
312}
313