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(
52            ThumbnailLoadTask task, final AttachmentBitmapHolder holder,
53            final Attachment attachment, final Attachment prevAttachment) {
54        final int width = holder.getThumbnailWidth();
55        final int height = holder.getThumbnailHeight();
56        if (attachment == null || width == 0 || height == 0
57                || !ImageUtils.isImageMimeType(attachment.getContentType())) {
58            holder.setThumbnailToDefault();
59            return;
60        }
61
62        final Uri thumbnailUri = attachment.thumbnailUri;
63        final Uri contentUri = attachment.contentUri;
64        final Uri uri = (prevAttachment == null) ? null : prevAttachment.getIdentifierUri();
65        final Uri prevUri = (prevAttachment == null) ? null : prevAttachment.getIdentifierUri();
66        // begin loading a thumbnail if this is an image and either the thumbnail or the original
67        // content is ready (and different from any existing image)
68        if ((thumbnailUri != null || contentUri != null)
69                && (holder.bitmapSetToDefault() ||
70                prevUri == null || !uri.equals(prevUri))) {
71            // cancel/dispose any existing task and start a new one
72            if (task != null) {
73                task.cancel(true);
74            }
75
76            task = new ThumbnailLoadTask(
77                    holder, width, height);
78            task.execute(thumbnailUri, contentUri);
79        } else if (thumbnailUri == null && contentUri == null) {
80            // not an image, or no thumbnail exists. fall back to default.
81            // async image load must separately ensure the default appears upon load failure.
82            holder.setThumbnailToDefault();
83        }
84    }
85
86    public ThumbnailLoadTask(AttachmentBitmapHolder holder, int width, int height) {
87        mHolder = holder;
88        mWidth = width;
89        mHeight = height;
90    }
91
92    @Override
93    protected Bitmap doInBackground(Uri... params) {
94        Bitmap result = loadBitmap(params[0]);
95        if (result == null) {
96            result = loadBitmap(params[1]);
97        }
98
99        return result;
100    }
101
102    private Bitmap loadBitmap(final Uri thumbnailUri) {
103        if (thumbnailUri == null) {
104            LogUtils.e(LOG_TAG, "Attempting to load bitmap for null uri");
105            return null;
106        }
107
108        final int orientation = getOrientation(thumbnailUri);
109
110        AssetFileDescriptor fd = null;
111        try {
112            fd = mHolder.getResolver().openAssetFileDescriptor(thumbnailUri, "r");
113            if (isCancelled() || fd == null) {
114                return null;
115            }
116
117            final BitmapFactory.Options opts = new BitmapFactory.Options();
118            opts.inJustDecodeBounds = true;
119            opts.inDensity = DisplayMetrics.DENSITY_LOW;
120
121            BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
122            if (isCancelled() || opts.outWidth == -1 || opts.outHeight == -1) {
123                return null;
124            }
125
126            opts.inJustDecodeBounds = false;
127            // Shrink both X and Y (but do not over-shrink)
128            // and pick the least affected dimension to ensure the thumbnail is fillable
129            // (i.e. ScaleType.CENTER_CROP)
130            final int wDivider = Math.max(opts.outWidth / mWidth, 1);
131            final int hDivider = Math.max(opts.outHeight / mHeight, 1);
132            opts.inSampleSize = Math.min(wDivider, hDivider);
133
134            LogUtils.d(LOG_TAG, "in background, src w/h=%d/%d dst w/h=%d/%d, divider=%d",
135                    opts.outWidth, opts.outHeight, mWidth, mHeight, opts.inSampleSize);
136
137            final Bitmap originalBitmap = BitmapFactory.decodeFileDescriptor(
138                    fd.getFileDescriptor(), null, opts);
139            if (originalBitmap != null && orientation != 0) {
140                final Matrix matrix = new Matrix();
141                matrix.postRotate(orientation);
142                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
143                        originalBitmap.getHeight(), matrix, true);
144            }
145            return originalBitmap;
146        } catch (Throwable t) {
147            LogUtils.i(LOG_TAG, "Unable to decode thumbnail %s: %s %s", thumbnailUri,
148                    t.getClass(), t.getMessage());
149        } finally {
150            if (fd != null) {
151                try {
152                    fd.close();
153                } catch (IOException e) {
154                    LogUtils.e(LOG_TAG, e, "");
155                }
156            }
157        }
158
159        return null;
160    }
161
162    private int getOrientation(final Uri thumbnailUri) {
163        if (thumbnailUri == null) {
164            return 0;
165        }
166
167        InputStream in = null;
168        try {
169            final ContentResolver resolver = mHolder.getResolver();
170            in = resolver.openInputStream(thumbnailUri);
171            return Exif.getOrientation(in, -1);
172        } catch (Throwable t) {
173            LogUtils.i(LOG_TAG, "Unable to get orientation of thumbnail %s: %s %s", thumbnailUri,
174                    t.getClass(), t.getMessage());
175        } finally {
176            if (in != null) {
177                try {
178                    in.close();
179                } catch (IOException e) {
180                    LogUtils.e(LOG_TAG, e, "error attemtping to close input stream");
181                }
182            }
183        }
184
185        return 0;
186    }
187
188    @Override
189    protected void onPostExecute(Bitmap result) {
190        if (result == null) {
191            LogUtils.d(LOG_TAG, "back in UI thread, decode failed or file does not exist");
192            mHolder.thumbnailLoadFailed();
193            return;
194        }
195
196        LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(),
197                result.getHeight());
198        mHolder.setThumbnail(result);
199    }
200
201}
202