| package com.android.mail.photomanager; |
| |
| import com.android.mail.photomanager.BitmapUtil.InputStreamFactory; |
| import com.android.mail.providers.Attachment; |
| import com.android.mail.providers.UIProvider; |
| import com.android.mail.providers.UIProvider.AttachmentRendition; |
| import com.android.mail.ui.DividedImageCanvas; |
| import com.android.mail.ui.ImageCanvas.Dimensions; |
| import com.android.mail.utils.Utils; |
| import com.google.common.base.Objects; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| |
| import com.android.mail.ui.ImageCanvas; |
| import com.android.mail.utils.LogUtils; |
| import com.google.common.primitives.Longs; |
| |
| import java.io.FileNotFoundException; |
| import java.io.InputStream; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * Asynchronously loads attachment image previews and maintains a cache of |
| * photos. |
| */ |
| public class AttachmentPreviewsManager extends PhotoManager { |
| |
| private static final DefaultImageProvider sDefaultImageProvider |
| = new AttachmentPreviewsDefaultProvider(); |
| private final Map<Object, AttachmentPreviewsManagerCallback> mCallbacks; |
| |
| public static int generateHash(ImageCanvas view, Object key) { |
| return Objects.hashCode(view, key); |
| } |
| |
| public static String transformKeyToUri(Object key) { |
| return (String) ((Pair)key).second; |
| } |
| |
| public AttachmentPreviewsManager(Context context) { |
| super(context); |
| mCallbacks = new HashMap<Object, AttachmentPreviewsManagerCallback>(); |
| } |
| |
| public void loadThumbnail(PhotoIdentifier id, ImageCanvas view, Dimensions dimensions, |
| AttachmentPreviewsManagerCallback callback) { |
| mCallbacks.put(id.getKey(), callback); |
| super.loadThumbnail(id, view, dimensions); |
| } |
| |
| @Override |
| protected DefaultImageProvider getDefaultImageProvider() { |
| return sDefaultImageProvider; |
| } |
| |
| @Override |
| protected int getHash(PhotoIdentifier id, ImageCanvas view) { |
| return generateHash(view, id.getKey()); |
| } |
| |
| @Override |
| protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) { |
| return new AttachmentPreviewsLoaderThread(contentResolver); |
| } |
| |
| @Override |
| protected void onImageDrawn(Request request, boolean success) { |
| Object key = request.getKey(); |
| if (mCallbacks.containsKey(key)) { |
| AttachmentPreviewsManagerCallback callback = mCallbacks.get(key); |
| callback.onImageDrawn(request.getKey(), success); |
| |
| if (success) { |
| mCallbacks.remove(key); |
| } |
| } |
| } |
| |
| @Override |
| protected void onImageLoadStarted(final Request request) { |
| if (request == null) { |
| return; |
| } |
| final Object key = request.getKey(); |
| if (mCallbacks.containsKey(key)) { |
| AttachmentPreviewsManagerCallback callback = mCallbacks.get(key); |
| callback.onImageLoadStarted(request.getKey()); |
| } |
| } |
| |
| @Override |
| protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) { |
| float ratio = (float) newWidth / prevWidth; |
| boolean previousRequestSmaller = newWidth > prevWidth |
| || newWidth > prevWidth * ratio |
| || newHeight > prevHeight * ratio; |
| return !previousRequestSmaller; |
| } |
| |
| public static class AttachmentPreviewIdentifier extends PhotoIdentifier { |
| public final String uri; |
| public final int rendition; |
| // conversationId and index used for sorting requests |
| long conversationId; |
| public int index; |
| |
| /** |
| * <RENDITION, URI> |
| */ |
| private Pair<Integer, String> mKey; |
| |
| public AttachmentPreviewIdentifier(String uri, int rendition, long conversationId, |
| int index) { |
| this.uri = uri; |
| this.rendition = rendition; |
| this.conversationId = conversationId; |
| this.index = index; |
| mKey = new Pair<Integer, String>(rendition, uri) { |
| @Override |
| public String toString() { |
| return "<" + first + ", " + second + ">"; |
| } |
| }; |
| } |
| |
| @Override |
| public boolean isValid() { |
| return !TextUtils.isEmpty(uri) && rendition >= AttachmentRendition.SIMPLE; |
| } |
| |
| @Override |
| public Object getKey() { |
| return mKey; |
| } |
| |
| @Override |
| public Object getKeyToShowInsteadOfDefault() { |
| return new AttachmentPreviewIdentifier(uri, rendition - 1, conversationId, index) |
| .getKey(); |
| } |
| |
| @Override |
| public int hashCode() { |
| int hash = 17; |
| hash = 31 * hash + (uri != null ? uri.hashCode() : 0); |
| hash = 31 * hash + rendition; |
| hash = 31 * hash + Longs.hashCode(conversationId); |
| hash = 31 * hash + index; |
| return hash; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| |
| AttachmentPreviewIdentifier that = (AttachmentPreviewIdentifier) o; |
| |
| if (rendition != that.rendition) { |
| return false; |
| } |
| if (uri != null ? !uri.equals(that.uri) : that.uri != null) { |
| return false; |
| } |
| if (conversationId != that.conversationId) { |
| return false; |
| } |
| if (index != that.index) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| return mKey.toString(); |
| } |
| |
| @Override |
| public int compareTo(PhotoIdentifier another) { |
| if (another instanceof AttachmentPreviewIdentifier) { |
| AttachmentPreviewIdentifier anotherId = (AttachmentPreviewIdentifier) another; |
| // We want to load SIMPLE images first because they are super fast |
| if (rendition - anotherId.rendition != 0) { |
| return rendition - anotherId.rendition; |
| } |
| |
| // Load images from later messages first (later messages appear on top of the list) |
| if (anotherId.conversationId - conversationId != 0) { |
| return (anotherId.conversationId - conversationId) > 0 ? 1 : -1; |
| } |
| |
| // Load images from left to right |
| if (index - anotherId.index != 0) { |
| return index - anotherId.index; |
| } |
| |
| return 0; |
| } else { |
| return -1; |
| } |
| } |
| } |
| |
| protected class AttachmentPreviewsLoaderThread extends PhotoLoaderThread { |
| |
| public AttachmentPreviewsLoaderThread(ContentResolver resolver) { |
| super(resolver); |
| } |
| |
| @Override |
| protected int getMaxBatchCount() { |
| return 1; |
| } |
| |
| @Override |
| protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) { |
| final Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>( |
| requests.size()); |
| |
| LogUtils.d(TAG, "AttachmentPreviewsManager: starting batch load. Count: %d", |
| requests.size()); |
| for (final Request request : requests) { |
| Utils.traceBeginSection("Setup load photo"); |
| final AttachmentPreviewIdentifier id = (AttachmentPreviewIdentifier) request |
| .getPhotoIdentifier(); |
| final Uri uri = Uri.parse(id.uri); |
| // Get the attachment for this preview |
| final Cursor cursor = getResolver() |
| .query(uri, UIProvider.ATTACHMENT_PROJECTION, null, null, null); |
| if (cursor == null) { |
| Utils.traceEndSection(); |
| continue; |
| } |
| Attachment attachment = null; |
| try { |
| LogUtils.v(TAG, "AttachmentPreviewsManager: found %d attachments for uri %s", |
| cursor.getCount(), uri); |
| if (cursor.moveToFirst()) { |
| attachment = new Attachment(cursor); |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| if (attachment == null) { |
| LogUtils.w(TAG, "AttachmentPreviewsManager: attachment not found for uri %s", |
| uri); |
| Utils.traceEndSection(); |
| continue; |
| } |
| |
| // Determine whether we load the SIMPLE or BEST image for this preview |
| final Uri contentUri; |
| if (id.rendition == UIProvider.AttachmentRendition.BEST) { |
| contentUri = attachment.contentUri; |
| } else if (id.rendition == AttachmentRendition.SIMPLE) { |
| contentUri = attachment.thumbnailUri; |
| } else { |
| LogUtils.w(TAG, |
| "AttachmentPreviewsManager: Cannot load rendition %d for uri %s", |
| id.rendition, uri); |
| Utils.traceEndSection(); |
| continue; |
| } |
| |
| LogUtils.v(TAG, "AttachmentPreviewsManager: attachments has contentUri %s", |
| contentUri); |
| final InputStreamFactory factory = new InputStreamFactory() { |
| @Override |
| public InputStream newInputStream() { |
| try { |
| return getResolver().openInputStream(contentUri); |
| } catch (FileNotFoundException e) { |
| LogUtils.e(TAG, |
| "AttachmentPreviewsManager: file not found for attachment %s." |
| + " This may be due to the attachment not being " |
| + "downloaded yet. But this shouldn't happen because " |
| + "we check the state of the attachment downloads " |
| + "before attempting to load it.", |
| contentUri); |
| return null; |
| } |
| } |
| }; |
| Utils.traceEndSection(); |
| |
| Utils.traceBeginSection("Decode stream and crop"); |
| // todo:markwei read EXIF data for orientation |
| // Crop it. I've seen that in real-world situations, |
| // a 5.5MB image will be cropped down to about a 200KB image, |
| // so this is definitely worth it. |
| final Bitmap bitmap = BitmapUtil |
| .decodeStreamWithCenterCrop(factory, request.bitmapKey.w, |
| request.bitmapKey.h); |
| Utils.traceEndSection(); |
| |
| if (bitmap == null) { |
| LogUtils.w(TAG, "Unable to decode bitmap for contentUri %s", contentUri); |
| continue; |
| } |
| cacheBitmap(request.bitmapKey, bitmap); |
| LogUtils.d(TAG, |
| "AttachmentPreviewsManager: finished loading attachment cropped size %db", |
| bitmap.getByteCount()); |
| } |
| |
| return photos; |
| } |
| } |
| |
| public static class AttachmentPreviewsDividedImageCanvas extends DividedImageCanvas { |
| public AttachmentPreviewsDividedImageCanvas(Context context, InvalidateCallback callback) { |
| super(context, callback); |
| } |
| |
| @Override |
| protected void drawVerticalDivider(int width, int height) { |
| return; // do not draw vertical dividers |
| } |
| |
| @Override |
| protected boolean isPartialBitmapComplete() { |
| return true; // images may not be loaded at the same time |
| } |
| |
| @Override |
| protected String transformKeyToDivisionId(Object key) { |
| return transformKeyToUri(key); |
| } |
| } |
| |
| public static class AttachmentPreviewsDefaultProvider implements DefaultImageProvider { |
| |
| /** |
| * All we need to do is clear the section. The ConversationItemView will draw the |
| * progress bar. |
| */ |
| @Override |
| public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) { |
| AttachmentPreviewsDividedImageCanvas dividedImageCanvas |
| = (AttachmentPreviewsDividedImageCanvas) view; |
| dividedImageCanvas.clearDivisionImage(id.getKey()); |
| } |
| } |
| |
| public interface AttachmentPreviewsManagerCallback { |
| |
| public void onImageDrawn(Object key, boolean success); |
| |
| public void onImageLoadStarted(Object key); |
| } |
| } |