blob: 35585a6d11126f563cc057403289c588f1632a52 [file] [log] [blame]
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov022b62d2011-01-28 12:16:07 -080017package com.android.contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080018
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080019import com.android.contacts.model.AccountTypeManager;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080020import com.android.contacts.util.MemoryUtils;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070021import com.android.contacts.util.UriUtils;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080022import com.google.android.collect.Lists;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010023import com.google.android.collect.Sets;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080024
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080025import android.content.ComponentCallbacks2;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080026import android.content.ContentResolver;
Dave Santoro84cac442011-08-24 15:23:10 -070027import android.content.ContentUris;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080028import android.content.Context;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080029import android.content.res.Configuration;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080030import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.graphics.BitmapFactory;
Daniel Lehmann7a46cde2012-02-01 17:53:44 -080033import android.graphics.drawable.BitmapDrawable;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070034import android.graphics.drawable.ColorDrawable;
35import android.graphics.drawable.Drawable;
Daniel Lehmann7a46cde2012-02-01 17:53:44 -080036import android.graphics.drawable.TransitionDrawable;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070037import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080038import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070039import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080040import android.os.HandlerThread;
41import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080042import android.provider.ContactsContract;
43import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080044import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070045import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080046import android.provider.ContactsContract.Directory;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080047import android.text.TextUtils;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070048import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080049import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080050import android.widget.ImageView;
51
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070052import java.io.ByteArrayOutputStream;
53import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070054import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080055import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080056import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010057import java.util.List;
58import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080059import java.util.concurrent.ConcurrentHashMap;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080060import java.util.concurrent.atomic.AtomicInteger;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080061
62/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080063 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080064 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080065public abstract class ContactPhotoManager implements ComponentCallbacks2 {
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080066 static final String TAG = "ContactPhotoManager";
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080067 static final boolean DEBUG = false; // Don't submit with true
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080068
69 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
70
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070071 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
72 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
73 if (hires) return R.drawable.ic_contact_picture_180_holo_light;
74 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
75 return R.drawable.ic_contact_picture_holo_light;
76 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080077
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070078 public static abstract class DefaultImageProvider {
79 public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
80 }
81
82 private static class AvatarDefaultImageProvider extends DefaultImageProvider {
83 @Override
84 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
85 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
86 }
87 }
88
89 private static class BlankDefaultImageProvider extends DefaultImageProvider {
90 private static Drawable sDrawable;
91
92 @Override
93 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
94 if (sDrawable == null) {
95 Context context = view.getContext();
96 sDrawable = new ColorDrawable(context.getResources().getColor(
97 R.color.image_placeholder));
98 }
99 view.setImageDrawable(sDrawable);
100 }
101 }
102
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800103 public static final DefaultImageProvider DEFAULT_AVATAR = new AvatarDefaultImageProvider();
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700104
105 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
106
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800107 /**
108 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
109 * the available authenticators. This method can safely be called from the UI thread.
110 */
111 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100112 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800113 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +0100114 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800115 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100116 service = createContactPhotoManager(applicationContext);
117 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800118 }
119 return service;
120 }
121
122 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
123 return new ContactPhotoManagerImpl(context);
124 }
125
126 /**
127 * Load photo into the supplied image view. If the photo is already cached,
128 * it is displayed immediately. Otherwise a request is sent to load the photo
129 * from the database.
130 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700131 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
132 DefaultImageProvider defaultProvider);
133
134 /**
135 * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800136 * {@link #DEFAULT_AVATAR}.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700137 */
138 public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800139 loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATAR);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700140 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800141
142 /**
143 * Load photo into the supplied image view. If the photo is already cached,
144 * it is displayed immediately. Otherwise a request is sent to load the photo
145 * from the location specified by the URI.
146 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700147 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
148 DefaultImageProvider defaultProvider);
149
150 /**
151 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800152 * {@link #DEFAULT_AVATAR}.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700153 */
154 public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800155 loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATAR);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700156 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800157
158 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700159 * Remove photo from the supplied image view. This also cancels current pending load request
160 * inside this photo manager.
161 */
162 public abstract void removePhoto(ImageView view);
163
164 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800165 * Temporarily stops loading photos from the database.
166 */
167 public abstract void pause();
168
169 /**
170 * Resumes loading photos from the database.
171 */
172 public abstract void resume();
173
174 /**
175 * Marks all cached photos for reloading. We can continue using cache but should
176 * also make sure the photos haven't changed in the background and notify the views
177 * if so.
178 */
179 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800180
181 /**
Dave Santoro14d20832011-12-02 18:14:52 -0800182 * Stores the given bitmap directly in the LRU bitmap cache.
183 * @param photoUri The URI of the photo (for future requests).
184 * @param bitmap The bitmap.
185 * @param photoBytes The bytes that were parsed to create the bitmap.
186 */
187 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
188
189 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800190 * Initiates a background process that over time will fill up cache with
191 * preload photos.
192 */
193 public abstract void preloadPhotosInBackground();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800194
195 // ComponentCallbacks2
196 @Override
197 public void onConfigurationChanged(Configuration newConfig) {
198 }
199
200 // ComponentCallbacks2
201 @Override
202 public void onLowMemory() {
203 }
204
205 // ComponentCallbacks2
206 @Override
207 public void onTrimMemory(int level) {
208 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800209}
210
211class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800212 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
213
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800214 private static final int FADE_TRANSITION_DURATION = 200;
215
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800216 /**
217 * Type of message sent by the UI thread to itself to indicate that some photos
218 * need to be loaded.
219 */
220 private static final int MESSAGE_REQUEST_LOADING = 1;
221
222 /**
223 * Type of message sent by the loader thread to indicate that some photos have
224 * been loaded.
225 */
226 private static final int MESSAGE_PHOTOS_LOADED = 2;
227
228 private static final String[] EMPTY_STRING_ARRAY = new String[0];
229
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800230 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800231
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800232 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800233 * Maintains the state of a particular photo.
234 */
235 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800236 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800237
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800238 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800239 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700240 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800241
242 public BitmapHolder(byte[] bytes) {
243 this.bytes = bytes;
244 this.fresh = true;
245 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800246 }
247
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800248 private final Context mContext;
249
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800250 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800251 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800252 * as they come from the database. Each holder has a soft reference to the
253 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800254 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800255 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
256
257 /**
Makoto Onuki1f0239e2012-02-27 17:13:47 -0800258 * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
259 */
260 private volatile boolean mBitmapHolderCacheAllUnfresh = true;
261
262 /**
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800263 * Cache size threshold at which bitmaps will not be preloaded.
264 */
265 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800266
267 /**
268 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
269 * the most recently used bitmaps to save time on decoding
270 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
271 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800272 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800273
274 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700275 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
276 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800277 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700278 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
279 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800280
281 /**
282 * Handler for messages sent to the UI thread.
283 */
284 private final Handler mMainThreadHandler = new Handler(this);
285
286 /**
287 * Thread responsible for loading photos from the database. Created upon
288 * the first request.
289 */
290 private LoaderThread mLoaderThread;
291
292 /**
293 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
294 */
295 private boolean mLoadingRequested;
296
297 /**
298 * Flag indicating if the image loading is paused.
299 */
300 private boolean mPaused;
301
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800302 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
303 private static final int HOLDER_CACHE_SIZE = 2000000;
304
305 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
306 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
307
308 private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
309
310 /** For debug: How many times we had to reload cached photo for a stale entry */
311 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
312
313 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
314 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
315
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800316 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800317 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800318
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800319 final float cacheSizeAdjustment =
320 (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
321 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
322 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
323 @Override protected int sizeOf(Object key, Bitmap value) {
324 return value.getByteCount();
325 }
326
327 @Override protected void entryRemoved(
328 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
329 if (DEBUG) dumpStats();
330 }
331 };
332 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
333 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800334 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800335 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800336 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800337
338 @Override protected void entryRemoved(
339 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
340 if (DEBUG) dumpStats();
341 }
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800342 };
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800343 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
344 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
345 if (DEBUG) {
346 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
347 + " + " + btk(mBitmapCache.maxSize()));
348 }
349 }
350
351 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
352 private static String btk(int bytes) {
353 return ((bytes + 1023) / 1024) + "K";
354 }
355
356 private static final int safeDiv(int dividend, int divisor) {
357 return (divisor == 0) ? 0 : (dividend / divisor);
358 }
359
360 /**
361 * Dump cache stats on logcat.
362 */
363 private void dumpStats() {
364 if (!DEBUG) return;
365 {
366 int numHolders = 0;
367 int rawBytes = 0;
368 int bitmapBytes = 0;
369 int numBitmaps = 0;
370 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
371 numHolders++;
372 if (h.bytes != null) {
373 rawBytes += h.bytes.length;
374 }
375 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
376 if (b != null) {
377 numBitmaps++;
378 bitmapBytes += b.getByteCount();
379 }
380 }
381 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
382 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
383 + numBitmaps + " bitmaps, avg: "
384 + btk(safeDiv(rawBytes, numHolders))
385 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
386 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
387 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
388 + " stale=" + mStaleCacheOverwrite.get());
389 }
390
391 {
392 int numBitmaps = 0;
393 int bitmapBytes = 0;
394 for (Bitmap b : mBitmapCache.snapshot().values()) {
395 numBitmaps++;
396 bitmapBytes += b.getByteCount();
397 }
398 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
399 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
400 // We don't get from L2 cache, so L2 stats is meaningless.
401 }
402 }
403
404 @Override
405 public void onTrimMemory(int level) {
406 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
407 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
408 // Clear the caches. Note all pending requests will be removed too.
409 clear();
410 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800411 }
412
413 @Override
414 public void preloadPhotosInBackground() {
415 ensureLoaderThread();
416 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800417 }
418
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800419 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700420 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
421 DefaultImageProvider defaultProvider) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800422 if (photoId == 0) {
423 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700424 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800425 mPendingRequests.remove(view);
426 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800427 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700428 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
429 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700430 }
431 }
432
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800433 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700434 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
435 DefaultImageProvider defaultProvider) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700436 if (photoUri == null) {
437 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700438 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700439 mPendingRequests.remove(view);
440 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800441 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700442 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
443 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700444 }
445 }
446
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700447 private void loadPhotoByIdOrUri(ImageView view, Request request) {
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800448 boolean loaded = loadCachedPhoto(view, request, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700449 if (loaded) {
450 mPendingRequests.remove(view);
451 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700452 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700453 if (!mPaused) {
454 // Send a request to start loading photos
455 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800456 }
457 }
458 }
459
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800460 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700461 public void removePhoto(ImageView view) {
462 view.setImageDrawable(null);
463 mPendingRequests.remove(view);
464 }
465
466 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800467 public void refreshCache() {
Makoto Onuki1f0239e2012-02-27 17:13:47 -0800468 if (mBitmapHolderCacheAllUnfresh) {
469 if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
470 return;
471 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800472 if (DEBUG) Log.d(TAG, "refreshCache");
Makoto Onuki1f0239e2012-02-27 17:13:47 -0800473 mBitmapHolderCacheAllUnfresh = true;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800474 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800475 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800476 }
477 }
478
479 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800480 * Checks if the photo is present in cache. If so, sets the photo on the view.
481 *
482 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800483 */
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800484 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
485 Bitmap bitmap = mBitmapCache.get(request.getKey());
486 if (bitmap != null) {
487 view.setImageBitmap(bitmap);
488 return true;
489 }
490
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700491 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800492 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800493 // The bitmap has not been loaded - should display the placeholder image.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700494 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800495 return false;
496 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800497
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800498 if (holder.bytes == null) {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700499 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800500 return holder.fresh;
501 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800502
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800503 // Optionally decode bytes into a bitmap
504 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800505
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800506 if (fadeIn) {
507 Drawable[] layers = new Drawable[2];
508 layers[0] = mContext.getResources().getDrawable(
509 getDefaultAvatarResId(request.mHires, request.mDarkTheme));
510 layers[1] = new BitmapDrawable(mContext.getResources(), holder.bitmap);
511 TransitionDrawable drawable = new TransitionDrawable(layers);
512 view.setImageDrawable(drawable);
513 drawable.startTransition(FADE_TRANSITION_DURATION);
514 } else {
515 view.setImageBitmap(holder.bitmap);
516 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800517
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700518 if (holder.bitmap != null) {
519 // Put the bitmap in the LRU cache
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800520 mBitmapCache.put(request.getKey(), holder.bitmap);
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700521 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800522
523 // Soften the reference
524 holder.bitmap = null;
525
526 return holder.fresh;
527 }
528
529 /**
530 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
531 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
532 * the holder, it will not be necessary to decode the bitmap.
533 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800534 private static void inflateBitmap(BitmapHolder holder) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800535 byte[] bytes = holder.bytes;
536 if (bytes == null || bytes.length == 0) {
537 return;
538 }
539
540 // Check the soft reference. If will be retained if the bitmap is also
541 // in the LRU cache, so we don't need to check the LRU cache explicitly.
542 if (holder.bitmapRef != null) {
543 holder.bitmap = holder.bitmapRef.get();
544 if (holder.bitmap != null) {
545 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800546 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800547 }
548
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800549 try {
550 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
551 holder.bitmap = bitmap;
552 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800553 if (DEBUG) {
554 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
555 + bitmap.getWidth() + "x" + bitmap.getHeight()
556 + ", " + btk(bitmap.getByteCount()));
557 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800558 } catch (OutOfMemoryError e) {
559 // Do nothing - the photo will appear to be missing
560 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800561 }
562
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700563 public void clear() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800564 if (DEBUG) Log.d(TAG, "clear");
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700565 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800566 mBitmapHolderCache.evictAll();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800567 mBitmapCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700568 }
569
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800570 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800571 public void pause() {
572 mPaused = true;
573 }
574
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800575 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800576 public void resume() {
577 mPaused = false;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800578 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800579 if (!mPendingRequests.isEmpty()) {
580 requestLoading();
581 }
582 }
583
584 /**
585 * Sends a message to this thread itself to start loading images. If the current
586 * view contains multiple image views, all of those image views will get a chance
587 * to request their respective photos before any of those requests are executed.
588 * This allows us to load images in bulk.
589 */
590 private void requestLoading() {
591 if (!mLoadingRequested) {
592 mLoadingRequested = true;
593 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
594 }
595 }
596
597 /**
598 * Processes requests on the main thread.
599 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700600 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800601 public boolean handleMessage(Message msg) {
602 switch (msg.what) {
603 case MESSAGE_REQUEST_LOADING: {
604 mLoadingRequested = false;
605 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800606 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800607 mLoaderThread.requestLoading();
608 }
609 return true;
610 }
611
612 case MESSAGE_PHOTOS_LOADED: {
613 if (!mPaused) {
614 processLoadedImages();
615 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800616 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800617 return true;
618 }
619 }
620 return false;
621 }
622
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800623 public void ensureLoaderThread() {
624 if (mLoaderThread == null) {
625 mLoaderThread = new LoaderThread(mContext.getContentResolver());
626 mLoaderThread.start();
627 }
628 }
629
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800630 /**
631 * Goes over pending loading requests and displays loaded photos. If some of the
632 * photos still haven't been loaded, sends another request for image loading.
633 */
634 private void processLoadedImages() {
635 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
636 while (iterator.hasNext()) {
637 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700638 Request key = mPendingRequests.get(view);
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800639 boolean loaded = loadCachedPhoto(view, key, true);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800640 if (loaded) {
641 iterator.remove();
642 }
643 }
644
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800645 softenCache();
646
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800647 if (!mPendingRequests.isEmpty()) {
648 requestLoading();
649 }
650 }
651
652 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800653 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800654 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800655 */
656 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800657 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800658 holder.bitmap = null;
659 }
660 }
661
662 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800663 * Stores the supplied bitmap in cache.
664 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800665 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800666 if (DEBUG) {
667 BitmapHolder prev = mBitmapHolderCache.get(key);
668 if (prev != null && prev.bytes != null) {
669 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
670 if (prev.fresh) {
671 mFreshCacheOverwrite.incrementAndGet();
672 } else {
673 mStaleCacheOverwrite.incrementAndGet();
674 }
675 }
676 Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
677 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800678 BitmapHolder holder = new BitmapHolder(bytes);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800679
680 // Unless this image is being preloaded, decode it right away while
681 // we are still on the background thread.
682 if (!preloading) {
683 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800684 }
685
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800686 mBitmapHolderCache.put(key, holder);
Makoto Onuki1f0239e2012-02-27 17:13:47 -0800687 mBitmapHolderCacheAllUnfresh = false;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800688 }
689
Dave Santoro14d20832011-12-02 18:14:52 -0800690 @Override
691 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
Daniel Lehmann2d73de02012-02-21 19:51:38 -0800692 Request request = Request.createFromUri(photoUri, true, false, DEFAULT_AVATAR);
Dave Santoro14d20832011-12-02 18:14:52 -0800693 BitmapHolder holder = new BitmapHolder(photoBytes);
694 mBitmapHolderCache.put(request.getKey(), holder);
Makoto Onuki1f0239e2012-02-27 17:13:47 -0800695 mBitmapHolderCacheAllUnfresh = false;
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800696 mBitmapCache.put(request.getKey(), bitmap);
Dave Santoro14d20832011-12-02 18:14:52 -0800697 }
698
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800699 /**
700 * Populates an array of photo IDs that need to be loaded.
701 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100702 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
703 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800704 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800705 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700706 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800707
708 /*
709 * Since the call is made from the loader thread, the map could be
710 * changing during the iteration. That's not really a problem:
711 * ConcurrentHashMap will allow those changes to happen without throwing
712 * exceptions. Since we may miss some requests in the situation of
713 * concurrent change, we will need to check the map again once loading
714 * is complete.
715 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700716 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800717 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700718 Request request = iterator.next();
719 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800720 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700721 if (request.isUriRequest()) {
722 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700723 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700724 photoIds.add(request.mId);
725 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700726 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800727 }
728 }
729 }
730
731 /**
732 * The thread that performs loading of photos from the database.
733 */
734 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700735 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800736 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
737 private static final int MESSAGE_LOAD_PHOTOS = 1;
738
739 /**
740 * A pause between preload batches that yields to the UI thread.
741 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700742 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800743
744 /**
745 * Number of photos to preload per batch.
746 */
747 private static final int PRELOAD_BATCH = 25;
748
749 /**
750 * Maximum number of photos to preload. If the cache size is 2Mb and
751 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
752 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700753 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700754
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800755 private final ContentResolver mResolver;
756 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100757 private final Set<Long> mPhotoIds = Sets.newHashSet();
758 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
759 private final Set<Uri> mPhotoUris = Sets.newHashSet();
760 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800761
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800762 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700763 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800764
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800765 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
766 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
767 private static final int PRELOAD_STATUS_DONE = 2;
768
769 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
770
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800771 public LoaderThread(ContentResolver resolver) {
772 super(LOADER_THREAD_NAME);
773 mResolver = resolver;
774 }
775
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800776 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800777 if (mLoaderThreadHandler == null) {
778 mLoaderThreadHandler = new Handler(getLooper(), this);
779 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800780 }
781
782 /**
783 * Kicks off preloading of the next batch of photos on the background thread.
784 * Preloading will happen after a delay: we want to yield to the UI thread
785 * as much as possible.
786 * <p>
787 * If preloading is already complete, does nothing.
788 */
789 public void requestPreloading() {
790 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
791 return;
792 }
793
794 ensureHandler();
795 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
796 return;
797 }
798
799 mLoaderThreadHandler.sendEmptyMessageDelayed(
800 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
801 }
802
803 /**
804 * Sends a message to this thread to load requested photos. Cancels a preloading
805 * request, if any: we don't want preloading to impede loading of the photos
806 * we need to display now.
807 */
808 public void requestLoading() {
809 ensureHandler();
810 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
811 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800812 }
813
814 /**
815 * Receives the above message, loads photos and then sends a message
816 * to the main thread to process them.
817 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700818 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800819 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800820 switch (msg.what) {
821 case MESSAGE_PRELOAD_PHOTOS:
822 preloadPhotosInBackground();
823 break;
824 case MESSAGE_LOAD_PHOTOS:
825 loadPhotosInBackground();
826 break;
827 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800828 return true;
829 }
830
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800831 /**
832 * The first time it is called, figures out which photos need to be preloaded.
833 * Each subsequent call preloads the next batch of photos and requests
834 * another cycle of preloading after a delay. The whole process ends when
835 * we either run out of photos to preload or fill up cache.
836 */
837 private void preloadPhotosInBackground() {
838 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
839 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800840 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800841
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800842 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
843 queryPhotosForPreload();
844 if (mPreloadPhotoIds.isEmpty()) {
845 mPreloadStatus = PRELOAD_STATUS_DONE;
846 } else {
847 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
848 }
849 requestPreloading();
850 return;
851 }
852
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800853 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800854 mPreloadStatus = PRELOAD_STATUS_DONE;
855 return;
856 }
857
858 mPhotoIds.clear();
859 mPhotoIdsAsStrings.clear();
860
861 int count = 0;
862 int preloadSize = mPreloadPhotoIds.size();
863 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
864 preloadSize--;
865 count++;
866 Long photoId = mPreloadPhotoIds.get(preloadSize);
867 mPhotoIds.add(photoId);
868 mPhotoIdsAsStrings.add(photoId.toString());
869 mPreloadPhotoIds.remove(preloadSize);
870 }
871
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700872 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800873
874 if (preloadSize == 0) {
875 mPreloadStatus = PRELOAD_STATUS_DONE;
876 }
877
Makoto Onuki173f2812011-09-06 14:49:27 -0700878 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
879 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800880
881 requestPreloading();
882 }
883
884 private void queryPhotosForPreload() {
885 Cursor cursor = null;
886 try {
887 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
888 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700889 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
890 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800891 .build();
892 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
893 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
894 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700895 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800896
897 if (cursor != null) {
898 while (cursor.moveToNext()) {
899 // Insert them in reverse order, because we will be taking
900 // them from the end of the list for loading.
901 mPreloadPhotoIds.add(0, cursor.getLong(0));
902 }
903 }
904 } finally {
905 if (cursor != null) {
906 cursor.close();
907 }
908 }
909 }
910
911 private void loadPhotosInBackground() {
912 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700913 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800914 loadRemotePhotos();
915 requestPreloading();
916 }
917
918 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100919 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800920 return;
921 }
922
923 // Remove loaded photos from the preload queue: we don't want
924 // the preloading process to load them again.
925 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100926 for (Long id : mPhotoIds) {
927 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800928 }
929 if (mPreloadPhotoIds.isEmpty()) {
930 mPreloadStatus = PRELOAD_STATUS_DONE;
931 }
932 }
933
934 mStringBuilder.setLength(0);
935 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100936 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800937 if (i != 0) {
938 mStringBuilder.append(',');
939 }
940 mStringBuilder.append('?');
941 }
942 mStringBuilder.append(')');
943
944 Cursor cursor = null;
945 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800946 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
Dave Santoro84cac442011-08-24 15:23:10 -0700947 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800948 COLUMNS,
949 mStringBuilder.toString(),
950 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
951 null);
952
953 if (cursor != null) {
954 while (cursor.moveToNext()) {
955 Long id = cursor.getLong(0);
956 byte[] bytes = cursor.getBlob(1);
957 cacheBitmap(id, bytes, preloading);
958 mPhotoIds.remove(id);
959 }
960 }
961 } finally {
962 if (cursor != null) {
963 cursor.close();
964 }
965 }
966
Dave Santoro84cac442011-08-24 15:23:10 -0700967 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100968 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700969 if (ContactsContract.isProfileId(id)) {
970 Cursor profileCursor = null;
971 try {
972 profileCursor = mResolver.query(
973 ContentUris.withAppendedId(Data.CONTENT_URI, id),
974 COLUMNS, null, null, null);
975 if (profileCursor != null && profileCursor.moveToFirst()) {
976 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
977 preloading);
978 } else {
979 // Couldn't load a photo this way either.
980 cacheBitmap(id, null, preloading);
981 }
982 } finally {
983 if (profileCursor != null) {
984 profileCursor.close();
985 }
986 }
987 } else {
988 // Not a profile photo and not found - mark the cache accordingly
989 cacheBitmap(id, null, preloading);
990 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800991 }
992
993 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
994 }
995
996 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100997 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700998 if (mBuffer == null) {
999 mBuffer = new byte[BUFFER_SIZE];
1000 }
1001 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -08001002 if (DEBUG) Log.d(TAG, "Loading " + uri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001003 InputStream is = mResolver.openInputStream(uri);
1004 if (is != null) {
1005 ByteArrayOutputStream baos = new ByteArrayOutputStream();
1006 try {
1007 int size;
1008 while ((size = is.read(mBuffer)) != -1) {
1009 baos.write(mBuffer, 0, size);
1010 }
1011 } finally {
1012 is.close();
1013 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001014 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001015 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -07001016 } else {
1017 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001018 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001019 }
1020 } catch (Exception ex) {
1021 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001022 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001023 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -08001024 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001025 }
1026 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001027
1028 /**
1029 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1030 * light theme
1031 */
1032 private static final class Request {
1033 private final long mId;
1034 private final Uri mUri;
1035 private final boolean mDarkTheme;
1036 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001037 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001038
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001039 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
1040 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001041 mId = id;
1042 mUri = uri;
1043 mDarkTheme = darkTheme;
1044 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001045 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001046 }
1047
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001048 public static Request createFromId(long id, boolean hires, boolean darkTheme,
1049 DefaultImageProvider defaultProvider) {
1050 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001051 }
1052
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001053 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
1054 DefaultImageProvider defaultProvider) {
1055 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001056 }
1057
1058 public boolean isDarkTheme() {
1059 return mDarkTheme;
1060 }
1061
1062 public boolean isHires() {
1063 return mHires;
1064 }
1065
1066 public boolean isUriRequest() {
1067 return mUri != null;
1068 }
1069
1070 @Override
1071 public int hashCode() {
1072 if (mUri != null) return mUri.hashCode();
1073
1074 // copied over from Long.hashCode()
1075 return (int) (mId ^ (mId >>> 32));
1076 }
1077
1078 @Override
1079 public boolean equals(Object o) {
1080 if (!(o instanceof Request)) return false;
1081 final Request that = (Request) o;
1082 // Don't compare equality of mHires and mDarkTheme fields because these are only used
1083 // in the default contact photo case. When the contact does have a photo, the contact
1084 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
1085 // the photo request on the queue twice.
1086 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
1087 }
1088
1089 public Object getKey() {
1090 return mUri == null ? mId : mUri;
1091 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001092
1093 public void applyDefaultImage(ImageView view) {
1094 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
1095 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001096 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001097}