/* * 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 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 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(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(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 mPendingRequests = Collections.synchronizedMap( new HashMap()); /** * 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 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 ? "" : 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 { /** * 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 loadPhotos(Collection 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 loadRequests = new HashSet(); final Collection decodeRequests = new HashSet(); final PriorityQueue requests; synchronized (mPendingRequests) { requests = new PriorityQueue(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 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 { 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); } } }