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