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 18 package com.android.mail.ui; 19 20 import android.content.ContentResolver; 21 import android.content.res.AssetFileDescriptor; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.graphics.Matrix; 25 import android.net.Uri; 26 import android.os.AsyncTask; 27 import android.util.DisplayMetrics; 28 29 import com.android.ex.photo.util.Exif; 30 import com.android.ex.photo.util.ImageUtils; 31 32 import com.android.mail.providers.Attachment; 33 import com.android.mail.utils.LogTag; 34 import com.android.mail.utils.LogUtils; 35 36 import java.io.IOException; 37 import 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 */ 44 public 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 setupThumbnailPreview(AttachmentTile.AttachmentPreviewCache cache, AttachmentBitmapHolder holder, Attachment attachment, Attachment prevAttachment)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 ThumbnailLoadTask(AttachmentBitmapHolder holder, int width, int height)89 public ThumbnailLoadTask(AttachmentBitmapHolder holder, int width, int height) { 90 mHolder = holder; 91 mWidth = width; 92 mHeight = height; 93 } 94 95 @Override doInBackground(Uri... params)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 loadBitmap(final Uri thumbnailUri)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 getOrientation(final Uri thumbnailUri)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 onPostExecute(Bitmap result)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