blob: 94f809d564772af5f9ec716aba451d49830516f2 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.photomanager;
import android.content.ComponentCallbacks2;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.util.LruCache;
import com.android.mail.ui.ImageCanvas;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Asynchronously loads photos and maintains a cache of photos
*/
public abstract class PhotoManager implements ComponentCallbacks2, Callback {
/**
* Get the default image provider that draws while the photo is being
* loaded.
*/
protected abstract DefaultImageProvider getDefaultImageProvider();
/**
* Generate a hashcode unique to each request.
*/
protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
/**
* Return a specific implementation of PhotoLoaderThread.
*/
protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
/**
* Subclasses can implement this method to alert callbacks that images finished loading.
* @param request The original request made.
* @param success True if we successfully loaded the image from cache. False if we fell back
* to the default image.
*/
protected void onImageDrawn(final Request request, final boolean success) {
// Subclasses can choose to do something about this
}
/**
* Subclasses can implement this method to alert callbacks that images started loading.
* @param request The original request made.
*/
protected void onImageLoadStarted(final Request request) {
// Subclasses can choose to do something about this
}
/**
* Subclasses can implement this method to determine whether a previously loaded bitmap can
* be reused for a new canvas size.
* @param prevWidth The width of the previously loaded bitmap.
* @param prevHeight The height of the previously loaded bitmap.
* @param newWidth The width of the canvas this request is drawing on.
* @param newHeight The height of the canvas this request is drawing on.
* @return
*/
protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
return true;
}
protected final Context getContext() {
return mContext;
}
static final String TAG = "PhotoManager";
static final boolean DEBUG = false; // Don't submit with true
static final boolean DEBUG_SIZES = false; // Don't submit with true
private static final String LOADER_THREAD_NAME = "PhotoLoader";
/**
* Type of message sent by the UI thread to itself to indicate that some photos
* need to be loaded.
*/
private static final int MESSAGE_REQUEST_LOADING = 1;
/**
* Type of message sent by the loader thread to indicate that some photos have
* been loaded.
*/
private static final int MESSAGE_PHOTOS_LOADED = 2;
/**
* Type of message sent by the loader thread to indicate that
*/
private static final int MESSAGE_PHOTO_LOADING = 3;
public interface DefaultImageProvider {
/**
* Applies the default avatar to the DividedImageView. Extent is an
* indicator for the size (width or height). If darkTheme is set, the
* avatar is one that looks better on dark background
* @param id
*/
public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent);
}
/**
* Maintains the state of a particular photo.
*/
protected static class BitmapHolder {
byte[] bytes;
int width;
int height;
volatile boolean fresh;
public BitmapHolder(byte[] bytes, int width, int height) {
this.bytes = bytes;
this.width = width;
this.height = height;
this.fresh = true;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append(super.toString());
sb.append(" bytes=");
sb.append(bytes);
sb.append(" size=");
sb.append(bytes == null ? 0 : bytes.length);
sb.append(" width=");
sb.append(width);
sb.append(" height=");
sb.append(height);
sb.append(" fresh=");
sb.append(fresh);
sb.append("}");
return sb.toString();
}
}
// todo:ath caches should be member vars
/**
* An LRU cache for bitmap holders. The cache contains bytes for photos just
* as they come from the database. Each holder has a soft reference to the
* actual bitmap. The keys are decided by the implementation.
*/
private static final LruCache<Object, BitmapHolder> sBitmapHolderCache;
/**
* Level 2 LRU cache for bitmaps. This is a smaller cache that holds
* the most recently used bitmaps to save time on decoding
* them from bytes (the bytes are stored in {@link #sBitmapHolderCache}.
* The keys are decided by the implementation.
*/
private static final LruCache<BitmapIdentifier, Bitmap> sBitmapCache;
/** Cache size for {@link #sBitmapHolderCache} for devices with "large" RAM. */
private static final int HOLDER_CACHE_SIZE = 2000000;
/** Cache size for {@link #sBitmapCache} for devices with "large" RAM. */
private static final int BITMAP_CACHE_SIZE = 1024 * 1024 * 8; // 8MB
/** For debug: How many times we had to reload cached photo for a stale entry */
private static final AtomicInteger sStaleCacheOverwrite = new AtomicInteger();
/** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
private static final AtomicInteger sFreshCacheOverwrite = new AtomicInteger();
static {
final float cacheSizeAdjustment =
(MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
1.0f : 0.5f;
final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
sBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
@Override protected int sizeOf(Object key, BitmapHolder value) {
return value.bytes != null ? value.bytes.length : 0;
}
@Override protected void entryRemoved(
boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
if (DEBUG) dumpStats();
}
};
final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
sBitmapCache = new LruCache<BitmapIdentifier, Bitmap>(bitmapCacheSize) {
@Override protected int sizeOf(BitmapIdentifier key, Bitmap value) {
return value.getByteCount();
}
@Override protected void entryRemoved(
boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
if (DEBUG) dumpStats();
}
};
LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
if (DEBUG) {
LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize())
+ " + " + btk(sBitmapCache.maxSize()));
}
}
/**
* A map from ImageCanvas hashcode to the corresponding photo ID or uri,
* encapsulated in a request. The request may swapped out before the photo
* loading request is started.
*/
private final Map<Integer, Request> mPendingRequests = Collections.synchronizedMap(
new HashMap<Integer, Request>());
/**
* Handler for messages sent to the UI thread.
*/
private final Handler mMainThreadHandler = new Handler(this);
/**
* Thread responsible for loading photos from the database. Created upon
* the first request.
*/
private PhotoLoaderThread mLoaderThread;
/**
* A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
*/
private boolean mLoadingRequested;
/**
* Flag indicating if the image loading is paused.
*/
private boolean mPaused;
private final Context mContext;
public PhotoManager(Context context) {
mContext = context;
}
public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) {
loadThumbnail(id, view, null);
}
/**
* Load an image
*
* @param dimensions Preferred dimensions
*/
public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view,
final ImageCanvas.Dimensions dimensions) {
Utils.traceBeginSection("Load thumbnail");
final DefaultImageProvider defaultProvider = getDefaultImageProvider();
final Request request = new Request(id, defaultProvider, view, dimensions);
final int hashCode = request.hashCode();
if (!id.isValid()) {
// No photo is needed
request.applyDefaultImage();
onImageDrawn(request, false);
mPendingRequests.remove(hashCode);
} else if (mPendingRequests.containsKey(hashCode)) {
LogUtils.d(TAG, "load request dropped for %s", id);
} else {
if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey());
loadPhoto(hashCode, request);
}
Utils.traceEndSection();
}
private void loadPhoto(int hashCode, Request request) {
if (DEBUG) {
LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s",
request.getKey(),
request,
Thread.currentThread());
}
boolean loaded = loadCachedPhoto(request, false);
if (loaded) {
if (DEBUG) {
LogUtils.v(TAG, "image request, cache hit. request queue size=%s",
mPendingRequests.size());
}
} else {
if (DEBUG) {
LogUtils.d(TAG, "image request, cache miss: key=%s", request.getKey());
}
mPendingRequests.put(hashCode, request);
if (!mPaused) {
// Send a request to start loading photos
requestLoading();
}
}
}
/**
* Remove photo from the supplied image view. This also cancels current pending load request
* inside this photo manager.
*/
public void removePhoto(int hashcode) {
Request r = mPendingRequests.remove(hashcode);
if (r != null) {
LogUtils.d(TAG, "removed request %s", r.getKey());
}
}
private void ensureLoaderThread() {
if (mLoaderThread == null) {
mLoaderThread = getLoaderThread(mContext.getContentResolver());
mLoaderThread.start();
}
}
/**
* Checks if the photo is present in cache. If so, sets the photo on the view.
*
* @param request Determines which image to load from cache.
* @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass
* false if the Loader Thread hasn't made any attempts to
* load images yet.
* @return false if the photo needs to be (re)loaded from the provider.
*/
private boolean loadCachedPhoto(final Request request,
final boolean afterLoaderThreadFinished) {
Utils.traceBeginSection("Load cached photo");
final Bitmap cached = getCachedPhoto(request.bitmapKey);
if (cached != null) {
if (DEBUG) {
LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
afterLoaderThreadFinished ? "DECODED IMG READ"
: "DECODED IMG CACHE HIT",
request.getKey(),
cached.getByteCount(),
Thread.currentThread());
}
if (request.getView().getGeneration() == request.viewGeneration) {
request.getView().drawImage(cached, request.getKey());
onImageDrawn(request, true);
}
Utils.traceEndSection();
return true;
}
// We couldn't load the requested image, so try to load a replacement.
// This removes the flicker from SIMPLE to BEST transition.
final Object replacementKey = request.getPhotoIdentifier().getKeyToShowInsteadOfDefault();
if (replacementKey != null) {
final BitmapIdentifier replacementBitmapKey = new BitmapIdentifier(replacementKey,
request.bitmapKey.w, request.bitmapKey.h);
final Bitmap cachedReplacement = getCachedPhoto(replacementBitmapKey);
if (cachedReplacement != null) {
if (DEBUG) {
LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
afterLoaderThreadFinished ? "DECODED IMG READ"
: "DECODED IMG CACHE HIT",
replacementKey,
cachedReplacement.getByteCount(),
Thread.currentThread());
}
if (request.getView().getGeneration() == request.viewGeneration) {
request.getView().drawImage(cachedReplacement, request.getKey());
onImageDrawn(request, true);
}
Utils.traceEndSection();
return false;
}
}
// We couldn't load any image, so draw a default image
request.applyDefaultImage();
final BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
// Check if we loaded null bytes, which means we meant to not draw anything.
if (holder != null && holder.bytes == null) {
onImageDrawn(request, holder.fresh);
Utils.traceEndSection();
return holder.fresh;
}
Utils.traceEndSection();
return false;
}
/**
* Takes care of retrieving the Bitmap from both the decoded and holder caches.
*/
private static Bitmap getCachedPhoto(BitmapIdentifier bitmapKey) {
Utils.traceBeginSection("Get cached photo");
final Bitmap cached = sBitmapCache.get(bitmapKey);
Utils.traceEndSection();
return cached;
}
/**
* Temporarily stops loading photos from the database.
*/
public void pause() {
LogUtils.d(TAG, "%s paused.", getClass().getName());
mPaused = true;
}
/**
* Resumes loading photos from the database.
*/
public void resume() {
LogUtils.d(TAG, "%s resumed.", getClass().getName());
mPaused = false;
if (DEBUG) dumpStats();
if (!mPendingRequests.isEmpty()) {
requestLoading();
}
}
/**
* Sends a message to this thread itself to start loading images. If the current
* view contains multiple image views, all of those image views will get a chance
* to request their respective photos before any of those requests are executed.
* This allows us to load images in bulk.
*/
private void requestLoading() {
if (!mLoadingRequested) {
mLoadingRequested = true;
mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
}
}
/**
* Processes requests on the main thread.
*/
@Override
public boolean handleMessage(final Message msg) {
switch (msg.what) {
case MESSAGE_REQUEST_LOADING: {
mLoadingRequested = false;
if (!mPaused) {
ensureLoaderThread();
mLoaderThread.requestLoading();
}
return true;
}
case MESSAGE_PHOTOS_LOADED: {
processLoadedImages();
if (DEBUG) dumpStats();
return true;
}
case MESSAGE_PHOTO_LOADING: {
final int hashcode = msg.arg1;
final Request request = mPendingRequests.get(hashcode);
onImageLoadStarted(request);
return true;
}
}
return false;
}
/**
* Goes over pending loading requests and displays loaded photos. If some of the
* photos still haven't been loaded, sends another request for image loading.
*/
private void processLoadedImages() {
Utils.traceBeginSection("process loaded images");
final List<Integer> toRemove = Lists.newArrayList();
for (final Integer hash : mPendingRequests.keySet()) {
final Request request = mPendingRequests.get(hash);
final boolean loaded = loadCachedPhoto(request, true);
// Request can go through multiple attempts if the LoaderThread fails to load any
// images for it, or if the images it loads are evicted from the cache before we
// could access them in the main thread.
if (loaded || request.attempts > 2) {
toRemove.add(hash);
}
}
for (final Integer key : toRemove) {
mPendingRequests.remove(key);
}
if (!mPaused && !mPendingRequests.isEmpty()) {
LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.",
mPendingRequests.size());
requestLoading();
}
Utils.traceEndSection();
}
/**
* Stores the supplied bitmap in cache.
*/
private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) {
if (DEBUG) {
BitmapHolder prev = sBitmapHolderCache.get(cacheKey);
if (prev != null && prev.bytes != null) {
LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey
+ (prev.fresh ? " FRESH" : " stale"));
if (prev.fresh) {
sFreshCacheOverwrite.incrementAndGet();
} else {
sStaleCacheOverwrite.incrementAndGet();
}
}
LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", "
+ (holder.bytes == null ? "<null>" : btk(holder.bytes.length)));
}
sBitmapHolderCache.put(cacheKey, holder);
}
protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
sBitmapCache.put(bitmapKey, bitmap);
}
// ComponentCallbacks2
@Override
public void onConfigurationChanged(Configuration newConfig) {
}
// ComponentCallbacks2
@Override
public void onLowMemory() {
}
// ComponentCallbacks2
@Override
public void onTrimMemory(int level) {
if (DEBUG) LogUtils.d(TAG, "onTrimMemory: " + level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// Clear the caches. Note all pending requests will be removed too.
clear();
}
}
public void clear() {
if (DEBUG) LogUtils.d(TAG, "clear");
mPendingRequests.clear();
sBitmapHolderCache.evictAll();
sBitmapCache.evictAll();
}
/**
* Dump cache stats on logcat.
*/
private static void dumpStats() {
if (!DEBUG) {
return;
}
int numHolders = 0;
int rawBytes = 0;
int bitmapBytes = 0;
int numBitmaps = 0;
for (BitmapHolder h : sBitmapHolderCache.snapshot().values()) {
numHolders++;
if (h.bytes != null) {
rawBytes += h.bytes.length;
numBitmaps++;
}
}
LogUtils.d(TAG,
"L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
+ btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
+ numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numBitmaps)));
LogUtils.d(TAG, "L1 Stats: %s, overwrite: fresh=%s stale=%s", sBitmapHolderCache,
sFreshCacheOverwrite.get(), sStaleCacheOverwrite.get());
numBitmaps = 0;
bitmapBytes = 0;
for (Bitmap b : sBitmapCache.snapshot().values()) {
numBitmaps++;
bitmapBytes += b.getByteCount();
}
LogUtils.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: "
+ btk(safeDiv(bitmapBytes, numBitmaps)));
// We don't get from L2 cache, so L2 stats is meaningless.
}
/** Converts bytes to K bytes, rounding up. Used only for debug log. */
private static String btk(int bytes) {
return ((bytes + 1023) / 1024) + "K";
}
private static final int safeDiv(int dividend, int divisor) {
return (divisor == 0) ? 0 : (dividend / divisor);
}
public static abstract class PhotoIdentifier implements Comparable<PhotoIdentifier> {
/**
* If this returns false, the PhotoManager will not attempt to load the
* bitmap. Instead, the default image provider will be used.
*/
public abstract boolean isValid();
/**
* Identifies this request.
*/
public abstract Object getKey();
/**
* Replacement key to try to load from cache instead of drawing the default image. This
* is useful when we've already loaded a SIMPLE rendition, and are now loading the BEST
* rendition. We want the BEST image to appear seamlessly on top of the existing SIMPLE
* image.
*/
public Object getKeyToShowInsteadOfDefault() {
return null;
}
}
/**
* The thread that performs loading of photos from the database.
*/
protected abstract class PhotoLoaderThread extends HandlerThread implements Callback {
/**
* Return photos mapped from {@link Request#getKey()} to the photo for
* that request.
*/
protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests);
private static final int MESSAGE_LOAD_PHOTOS = 0;
private final ContentResolver mResolver;
private Handler mLoaderThreadHandler;
public PhotoLoaderThread(ContentResolver resolver) {
super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND);
mResolver = resolver;
}
protected ContentResolver getResolver() {
return mResolver;
}
public void ensureHandler() {
if (mLoaderThreadHandler == null) {
mLoaderThreadHandler = new Handler(getLooper(), this);
}
}
/**
* Sends a message to this thread to load requested photos. Cancels a preloading
* request, if any: we don't want preloading to impede loading of the photos
* we need to display now.
*/
public void requestLoading() {
ensureHandler();
mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
}
/**
* Receives the above message, loads photos and then sends a message
* to the main thread to process them.
*/
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_LOAD_PHOTOS:
loadPhotosInBackground();
break;
}
return true;
}
/**
* Subclasses may specify the maximum number of requests to be given at a time to
* #loadPhotos(). For batch count N, the UI will be updated with up to N images at a time.
*
* @return A positive integer if you would like to limit the number of
* items in a single batch.
*/
protected int getMaxBatchCount() {
return -1;
}
private void loadPhotosInBackground() {
Utils.traceBeginSection("pre processing");
final Collection<Request> loadRequests = new HashSet<PhotoManager.Request>();
final Collection<Request> decodeRequests = new HashSet<PhotoManager.Request>();
final PriorityQueue<Request> requests;
synchronized (mPendingRequests) {
requests = new PriorityQueue<Request>(mPendingRequests.values());
}
int batchCount = 0;
int maxBatchCount = getMaxBatchCount();
while (!requests.isEmpty()) {
Request request = requests.poll();
final BitmapHolder holder = sBitmapHolderCache
.get(request.getKey());
if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
holder.width, holder.height, request.bitmapKey.w, request.bitmapKey.h)) {
loadRequests.add(request);
decodeRequests.add(request);
batchCount++;
final Message msg = Message.obtain();
msg.what = MESSAGE_PHOTO_LOADING;
msg.arg1 = request.hashCode();
mMainThreadHandler.sendMessage(msg);
} else {
// Even if the image load is already done, this particular decode configuration
// may not yet have run. Be sure to add it to the queue.
if (sBitmapCache.get(request.bitmapKey) == null) {
decodeRequests.add(request);
}
}
request.attempts++;
if (maxBatchCount > 0 && batchCount >= maxBatchCount) {
break;
}
}
Utils.traceEndSection();
Utils.traceBeginSection("load photos");
// Ask subclass to do the actual loading
final Map<String, BitmapHolder> photosMap = loadPhotos(loadRequests);
Utils.traceEndSection();
if (DEBUG) {
LogUtils.d(TAG,
"worker thread completed read request batch. inputN=%s outputN=%s",
loadRequests.size(),
photosMap.size());
}
Utils.traceBeginSection("post processing");
for (String cacheKey : photosMap.keySet()) {
if (DEBUG) {
LogUtils.d(TAG,
"worker thread completed read request key=%s byteCount=%s thread=%s",
cacheKey,
photosMap.get(cacheKey) == null ? 0
: photosMap.get(cacheKey).bytes.length,
Thread.currentThread());
}
cacheBitmapHolder(cacheKey, photosMap.get(cacheKey));
}
for (Request r : decodeRequests) {
if (sBitmapCache.get(r.bitmapKey) != null) {
continue;
}
final Object cacheKey = r.getKey();
final BitmapHolder holder = sBitmapHolderCache.get(cacheKey);
if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
holder.width, holder.height, r.bitmapKey.w, r.bitmapKey.h)) {
continue;
}
final int w = r.bitmapKey.w;
final int h = r.bitmapKey.h;
final byte[] src = holder.bytes;
if (w == 0 || h == 0) {
LogUtils.e(TAG, new Error(), "bad dimensions for request=%s w/h=%s/%s",
r, w, h);
}
final Bitmap decoded = BitmapUtil.decodeByteArrayWithCenterCrop(src, w, h);
if (DEBUG) {
LogUtils.i(TAG,
"worker thread completed decode bmpKey=%s decoded=%s holder=%s",
r.bitmapKey, decoded, holder);
}
if (decoded != null) {
cacheBitmap(r.bitmapKey, decoded);
}
}
Utils.traceEndSection();
mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
}
protected String createInQuery(String value, int itemCount) {
// Build first query
StringBuilder query = new StringBuilder().append(value + " IN (");
appendQuestionMarks(query, itemCount);
query.append(')');
return query.toString();
}
protected void appendQuestionMarks(StringBuilder query, int itemCount) {
boolean first = true;
for (int i = 0; i < itemCount; i++) {
if (first) {
first = false;
} else {
query.append(',');
}
query.append('?');
}
}
}
/**
* An object to uniquely identify a combination of (Request + decoded size). Multiple requests
* may require the same src image, but want to decode it into different sizes.
*/
public static final class BitmapIdentifier {
public final Object key;
public final int w;
public final int h;
// OK to be static as long as all Requests are created on the same
// thread
private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions();
public static BitmapIdentifier getBitmapKey(PhotoIdentifier id, ImageCanvas view,
ImageCanvas.Dimensions dimensions) {
final int width;
final int height;
if (dimensions != null) {
width = dimensions.width;
height = dimensions.height;
} else {
view.getDesiredDimensions(id.getKey(), sWorkDims);
width = sWorkDims.width;
height = sWorkDims.height;
}
return new BitmapIdentifier(id.getKey(), width, height);
}
public BitmapIdentifier(Object key, int w, int h) {
this.key = key;
this.w = w;
this.h = h;
}
@Override
public int hashCode() {
int hash = 19;
hash = 31 * hash + key.hashCode();
hash = 31 * hash + w;
hash = 31 * hash + h;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
} else if (obj == this) {
return true;
}
final BitmapIdentifier o = (BitmapIdentifier) obj;
return Objects.equal(key, o.key) && w == o.w && h == o.h;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append(super.toString());
sb.append(" key=");
sb.append(key);
sb.append(" w=");
sb.append(w);
sb.append(" h=");
sb.append(h);
sb.append("}");
return sb.toString();
}
}
/**
* A holder for a contact photo request.
*/
public final class Request implements Comparable<Request> {
private final int mRequestedExtent;
private final DefaultImageProvider mDefaultProvider;
private final PhotoIdentifier mPhotoIdentifier;
private final ImageCanvas mView;
public final BitmapIdentifier bitmapKey;
public final int viewGeneration;
public int attempts;
private Request(final PhotoIdentifier photoIdentifier,
final DefaultImageProvider defaultProvider, final ImageCanvas view,
final ImageCanvas.Dimensions dimensions) {
mPhotoIdentifier = photoIdentifier;
mRequestedExtent = -1;
mDefaultProvider = defaultProvider;
mView = view;
viewGeneration = view.getGeneration();
bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions);
}
public ImageCanvas getView() {
return mView;
}
public PhotoIdentifier getPhotoIdentifier() {
return mPhotoIdentifier;
}
/**
* @see PhotoIdentifier#getKey()
*/
public Object getKey() {
return mPhotoIdentifier.getKey();
}
@Override
public int hashCode() {
return getHash(mPhotoIdentifier, mView);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final Request that = (Request) obj;
if (mRequestedExtent != that.mRequestedExtent) return false;
if (!Objects.equal(mPhotoIdentifier, that.mPhotoIdentifier)) return false;
if (!Objects.equal(mView, that.mView)) return false;
// Don't compare equality of mDarkTheme because it is only used in the default contact
// photo case. When the contact does have a photo, the contact photo is the same
// regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
// twice.
return true;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
sb.append(super.toString());
sb.append(" key=");
sb.append(getKey());
sb.append(" id=");
sb.append(mPhotoIdentifier);
sb.append(" mView=");
sb.append(mView);
sb.append(" mExtent=");
sb.append(mRequestedExtent);
sb.append(" bitmapKey=");
sb.append(bitmapKey);
sb.append(" viewGeneration=");
sb.append(viewGeneration);
sb.append("}");
return sb.toString();
}
public void applyDefaultImage() {
if (mView.getGeneration() != viewGeneration) {
// This can legitimately happen when an ImageCanvas is reused and re-purposed to
// house a new set of images (e.g. by ListView recycling).
// Ignore this now-stale request.
if (DEBUG) {
LogUtils.d(TAG,
"ImageCanvas skipping applyDefaultImage; no longer contains" +
" item=%s canvas=%s", getKey(), mView);
}
}
mDefaultProvider.applyDefaultImage(mPhotoIdentifier, mView, mRequestedExtent);
}
@Override
public int compareTo(Request another) {
// Hold off on loading Requests which have failed before so it don't hold up others
if (attempts - another.attempts != 0) {
return attempts - another.attempts;
}
return mPhotoIdentifier.compareTo(another.mPhotoIdentifier);
}
}
}