1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.content.ContentResolver;
21import android.content.res.AssetFileDescriptor;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.Matrix;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.util.DisplayMetrics;
28
29import com.android.ex.photo.util.Exif;
30import com.android.ex.photo.util.ImageUtils;
31
32import com.android.mail.providers.Attachment;
33import com.android.mail.utils.LogTag;
34import com.android.mail.utils.LogUtils;
35
36import java.io.IOException;
37import java.io.InputStream;
38
39/**
40 * Performs the load of a thumbnail bitmap in a background
41 * {@link AsyncTask}. Available for use with any view that implements
42 * the {@link AttachmentBitmapHolder} interface.
43 */
44public class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
45    private static final String LOG_TAG = LogTag.getLogTag();
46
47    private final AttachmentBitmapHolder mHolder;
48    private final int mWidth;
49    private final int mHeight;
50
51    public static void setupThumbnailPreview(AttachmentTile.AttachmentPreviewCache cache,
52            AttachmentBitmapHolder holder, Attachment attachment, Attachment prevAttachment) {
53        // Check cache first
54        if (cache != null) {
55            final Bitmap cached = cache.get(attachment);
56            if (cached != null) {
57                holder.setThumbnail(cached);
58                return;
59            }
60        }
61
62        final int width = holder.getThumbnailWidth();
63        final int height = holder.getThumbnailHeight();
64        if (attachment == null || width == 0 || height == 0
65                || !ImageUtils.isImageMimeType(attachment.getContentType())) {
66            holder.setThumbnailToDefault();
67            return;
68        }
69
70        final Uri thumbnailUri = attachment.thumbnailUri;
71        final Uri contentUri = attachment.contentUri;
72        final Uri uri = attachment.getIdentifierUri();
73        final Uri prevUri = (prevAttachment == null) ? null : prevAttachment.getIdentifierUri();
74        // begin loading a thumbnail if this is an image and either the thumbnail or the original
75        // content is ready (and different from any existing image)
76        if ((thumbnailUri != null || contentUri != null)
77                && (holder.bitmapSetToDefault() ||
78                prevUri == null || !uri.equals(prevUri))) {
79            final ThumbnailLoadTask task = new ThumbnailLoadTask(
80                    holder, width, height);
81            task.execute(thumbnailUri, contentUri);
82        } else if (thumbnailUri == null && contentUri == null) {
83            // not an image, or no thumbnail exists. fall back to default.
84            // async image load must separately ensure the default appears upon load failure.
85            holder.setThumbnailToDefault();
86        }
87    }
88
89    public ThumbnailLoadTask(AttachmentBitmapHolder holder, int width, int height) {
90        mHolder = holder;
91        mWidth = width;
92        mHeight = height;
93    }
94
95    @Override
96    protected Bitmap doInBackground(Uri... params) {
97        Bitmap result = loadBitmap(params[0]);
98        if (result == null) {
99            result = loadBitmap(params[1]);
100        }
101
102        return result;
103    }
104
105    private Bitmap loadBitmap(final Uri thumbnailUri) {
106        if (thumbnailUri == null) {
107            LogUtils.e(LOG_TAG, "Attempting to load bitmap for null uri");
108            return null;
109        }
110
111        final int orientation = getOrientation(thumbnailUri);
112
113        AssetFileDescriptor fd = null;
114        try {
115            fd = mHolder.getResolver().openAssetFileDescriptor(thumbnailUri, "r");
116            if (isCancelled() || fd == null) {
117                return null;
118            }
119
120            final BitmapFactory.Options opts = new BitmapFactory.Options();
121            opts.inJustDecodeBounds = true;
122            opts.inDensity = DisplayMetrics.DENSITY_LOW;
123
124            BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
125            if (isCancelled() || opts.outWidth == -1 || opts.outHeight == -1) {
126                return null;
127            }
128
129            opts.inJustDecodeBounds = false;
130            // Shrink both X and Y (but do not over-shrink)
131            // and pick the least affected dimension to ensure the thumbnail is fillable
132            // (i.e. ScaleType.CENTER_CROP)
133            final int wDivider = Math.max(opts.outWidth / mWidth, 1);
134            final int hDivider = Math.max(opts.outHeight / mHeight, 1);
135            opts.inSampleSize = Math.min(wDivider, hDivider);
136
137            LogUtils.d(LOG_TAG, "in background, src w/h=%d/%d dst w/h=%d/%d, divider=%d",
138                    opts.outWidth, opts.outHeight, mWidth, mHeight, opts.inSampleSize);
139
140            final Bitmap originalBitmap = BitmapFactory.decodeFileDescriptor(
141                    fd.getFileDescriptor(), null, opts);
142            if (originalBitmap != null && orientation != 0) {
143                final Matrix matrix = new Matrix();
144                matrix.postRotate(orientation);
145                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
146                        originalBitmap.getHeight(), matrix, true);
147            }
148            return originalBitmap;
149        } catch (Throwable t) {
150            LogUtils.i(LOG_TAG, "Unable to decode thumbnail %s: %s %s", thumbnailUri,
151                    t.getClass(), t.getMessage());
152        } finally {
153            if (fd != null) {
154                try {
155                    fd.close();
156                } catch (IOException e) {
157                    LogUtils.e(LOG_TAG, e, "");
158                }
159            }
160        }
161
162        return null;
163    }
164
165    private int getOrientation(final Uri thumbnailUri) {
166        if (thumbnailUri == null) {
167            return 0;
168        }
169
170        InputStream in = null;
171        try {
172            final ContentResolver resolver = mHolder.getResolver();
173            in = resolver.openInputStream(thumbnailUri);
174            return Exif.getOrientation(in, -1);
175        } catch (Throwable t) {
176            LogUtils.i(LOG_TAG, "Unable to get orientation of thumbnail %s: %s %s", thumbnailUri,
177                    t.getClass(), t.getMessage());
178        } finally {
179            if (in != null) {
180                try {
181                    in.close();
182                } catch (IOException e) {
183                    LogUtils.e(LOG_TAG, e, "error attemtping to close input stream");
184                }
185            }
186        }
187
188        return 0;
189    }
190
191    @Override
192    protected void onPostExecute(Bitmap result) {
193        if (result == null) {
194            LogUtils.d(LOG_TAG, "back in UI thread, decode failed or file does not exist");
195            mHolder.thumbnailLoadFailed();
196            return;
197        }
198
199        LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(),
200                result.getHeight());
201        mHolder.setThumbnail(result);
202    }
203
204}
205