blob: 10149e973356d364e0347a8c515bd71baff58e3d [file] [log] [blame]
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);
}
}