blob: f53766de1bdee61fb010cd056f29c15710f6b917 [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;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070033import android.graphics.drawable.ColorDrawable;
34import android.graphics.drawable.Drawable;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070035import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080036import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070037import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080038import android.os.HandlerThread;
39import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080040import android.provider.ContactsContract;
41import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080042import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070043import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080044import android.provider.ContactsContract.Directory;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080045import android.text.TextUtils;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070046import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080047import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080048import android.widget.ImageView;
49
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070050import java.io.ByteArrayOutputStream;
51import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070052import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080053import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080054import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010055import java.util.List;
56import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080057import java.util.concurrent.ConcurrentHashMap;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080058import java.util.concurrent.atomic.AtomicInteger;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080059
60/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080061 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080062 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080063public abstract class ContactPhotoManager implements ComponentCallbacks2 {
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080064 static final String TAG = "ContactPhotoManager";
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080065 static final boolean DEBUG = false; // Don't submit with true
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080066
67 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
68
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070069 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
70 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
71 if (hires) return R.drawable.ic_contact_picture_180_holo_light;
72 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
73 return R.drawable.ic_contact_picture_holo_light;
74 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080075
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070076 public static abstract class DefaultImageProvider {
77 public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
78 }
79
80 private static class AvatarDefaultImageProvider extends DefaultImageProvider {
81 @Override
82 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
83 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
84 }
85 }
86
87 private static class BlankDefaultImageProvider extends DefaultImageProvider {
88 private static Drawable sDrawable;
89
90 @Override
91 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
92 if (sDrawable == null) {
93 Context context = view.getContext();
94 sDrawable = new ColorDrawable(context.getResources().getColor(
95 R.color.image_placeholder));
96 }
97 view.setImageDrawable(sDrawable);
98 }
99 }
100
101 public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider();
102
103 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
104
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800105 /**
106 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
107 * the available authenticators. This method can safely be called from the UI thread.
108 */
109 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100110 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800111 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +0100112 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800113 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100114 service = createContactPhotoManager(applicationContext);
115 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800116 }
117 return service;
118 }
119
120 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
121 return new ContactPhotoManagerImpl(context);
122 }
123
124 /**
125 * Load photo into the supplied image view. If the photo is already cached,
126 * it is displayed immediately. Otherwise a request is sent to load the photo
127 * from the database.
128 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700129 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
130 DefaultImageProvider defaultProvider);
131
132 /**
133 * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
134 * {@link #DEFAULT_AVATER}.
135 */
136 public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
137 loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER);
138 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800139
140 /**
141 * Load photo into the supplied image view. If the photo is already cached,
142 * it is displayed immediately. Otherwise a request is sent to load the photo
143 * from the location specified by the URI.
144 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700145 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
146 DefaultImageProvider defaultProvider);
147
148 /**
149 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
150 * {@link #DEFAULT_AVATER}.
151 */
152 public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
153 loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER);
154 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800155
156 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700157 * Remove photo from the supplied image view. This also cancels current pending load request
158 * inside this photo manager.
159 */
160 public abstract void removePhoto(ImageView view);
161
162 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800163 * Temporarily stops loading photos from the database.
164 */
165 public abstract void pause();
166
167 /**
168 * Resumes loading photos from the database.
169 */
170 public abstract void resume();
171
172 /**
173 * Marks all cached photos for reloading. We can continue using cache but should
174 * also make sure the photos haven't changed in the background and notify the views
175 * if so.
176 */
177 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800178
179 /**
Dave Santoro14d20832011-12-02 18:14:52 -0800180 * Stores the given bitmap directly in the LRU bitmap cache.
181 * @param photoUri The URI of the photo (for future requests).
182 * @param bitmap The bitmap.
183 * @param photoBytes The bytes that were parsed to create the bitmap.
184 */
185 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
186
187 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800188 * Initiates a background process that over time will fill up cache with
189 * preload photos.
190 */
191 public abstract void preloadPhotosInBackground();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800192
193 // ComponentCallbacks2
194 @Override
195 public void onConfigurationChanged(Configuration newConfig) {
196 }
197
198 // ComponentCallbacks2
199 @Override
200 public void onLowMemory() {
201 }
202
203 // ComponentCallbacks2
204 @Override
205 public void onTrimMemory(int level) {
206 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800207}
208
209class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800210 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
211
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800212 /**
213 * Type of message sent by the UI thread to itself to indicate that some photos
214 * need to be loaded.
215 */
216 private static final int MESSAGE_REQUEST_LOADING = 1;
217
218 /**
219 * Type of message sent by the loader thread to indicate that some photos have
220 * been loaded.
221 */
222 private static final int MESSAGE_PHOTOS_LOADED = 2;
223
224 private static final String[] EMPTY_STRING_ARRAY = new String[0];
225
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800226 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800227
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800228 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800229 * Maintains the state of a particular photo.
230 */
231 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800232 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800233
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800234 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800235 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700236 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800237
238 public BitmapHolder(byte[] bytes) {
239 this.bytes = bytes;
240 this.fresh = true;
241 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800242 }
243
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800244 private final Context mContext;
245
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800246 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800247 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800248 * as they come from the database. Each holder has a soft reference to the
249 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800250 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800251 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
252
253 /**
254 * Cache size threshold at which bitmaps will not be preloaded.
255 */
256 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800257
258 /**
259 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
260 * the most recently used bitmaps to save time on decoding
261 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
262 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800263 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800264
265 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700266 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
267 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800268 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700269 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
270 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800271
272 /**
273 * Handler for messages sent to the UI thread.
274 */
275 private final Handler mMainThreadHandler = new Handler(this);
276
277 /**
278 * Thread responsible for loading photos from the database. Created upon
279 * the first request.
280 */
281 private LoaderThread mLoaderThread;
282
283 /**
284 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
285 */
286 private boolean mLoadingRequested;
287
288 /**
289 * Flag indicating if the image loading is paused.
290 */
291 private boolean mPaused;
292
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800293 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
294 private static final int HOLDER_CACHE_SIZE = 2000000;
295
296 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
297 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
298
299 private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
300
301 /** For debug: How many times we had to reload cached photo for a stale entry */
302 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
303
304 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
305 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
306
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800307 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800308 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800309
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800310 final float cacheSizeAdjustment =
311 (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
312 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
313 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
314 @Override protected int sizeOf(Object key, Bitmap value) {
315 return value.getByteCount();
316 }
317
318 @Override protected void entryRemoved(
319 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
320 if (DEBUG) dumpStats();
321 }
322 };
323 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
324 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800325 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800326 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800327 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800328
329 @Override protected void entryRemoved(
330 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
331 if (DEBUG) dumpStats();
332 }
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800333 };
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800334 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
335 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
336 if (DEBUG) {
337 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
338 + " + " + btk(mBitmapCache.maxSize()));
339 }
340 }
341
342 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
343 private static String btk(int bytes) {
344 return ((bytes + 1023) / 1024) + "K";
345 }
346
347 private static final int safeDiv(int dividend, int divisor) {
348 return (divisor == 0) ? 0 : (dividend / divisor);
349 }
350
351 /**
352 * Dump cache stats on logcat.
353 */
354 private void dumpStats() {
355 if (!DEBUG) return;
356 {
357 int numHolders = 0;
358 int rawBytes = 0;
359 int bitmapBytes = 0;
360 int numBitmaps = 0;
361 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
362 numHolders++;
363 if (h.bytes != null) {
364 rawBytes += h.bytes.length;
365 }
366 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
367 if (b != null) {
368 numBitmaps++;
369 bitmapBytes += b.getByteCount();
370 }
371 }
372 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
373 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
374 + numBitmaps + " bitmaps, avg: "
375 + btk(safeDiv(rawBytes, numHolders))
376 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
377 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
378 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
379 + " stale=" + mStaleCacheOverwrite.get());
380 }
381
382 {
383 int numBitmaps = 0;
384 int bitmapBytes = 0;
385 for (Bitmap b : mBitmapCache.snapshot().values()) {
386 numBitmaps++;
387 bitmapBytes += b.getByteCount();
388 }
389 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
390 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
391 // We don't get from L2 cache, so L2 stats is meaningless.
392 }
393 }
394
395 @Override
396 public void onTrimMemory(int level) {
397 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
398 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
399 // Clear the caches. Note all pending requests will be removed too.
400 clear();
401 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800402 }
403
404 @Override
405 public void preloadPhotosInBackground() {
406 ensureLoaderThread();
407 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800408 }
409
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800410 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700411 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
412 DefaultImageProvider defaultProvider) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800413 if (photoId == 0) {
414 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700415 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800416 mPendingRequests.remove(view);
417 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800418 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700419 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
420 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700421 }
422 }
423
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800424 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700425 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
426 DefaultImageProvider defaultProvider) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700427 if (photoUri == null) {
428 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700429 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700430 mPendingRequests.remove(view);
431 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800432 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700433 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
434 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700435 }
436 }
437
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700438 private void loadPhotoByIdOrUri(ImageView view, Request request) {
439 boolean loaded = loadCachedPhoto(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700440 if (loaded) {
441 mPendingRequests.remove(view);
442 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700443 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700444 if (!mPaused) {
445 // Send a request to start loading photos
446 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800447 }
448 }
449 }
450
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800451 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700452 public void removePhoto(ImageView view) {
453 view.setImageDrawable(null);
454 mPendingRequests.remove(view);
455 }
456
457 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800458 public void refreshCache() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800459 if (DEBUG) Log.d(TAG, "refreshCache");
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800460 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800461 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800462 }
463 }
464
465 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800466 * Checks if the photo is present in cache. If so, sets the photo on the view.
467 *
468 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800469 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700470 private boolean loadCachedPhoto(ImageView view, Request request) {
471 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800472 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800473 // The bitmap has not been loaded - should display the placeholder image.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700474 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800475 return false;
476 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800477
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800478 if (holder.bytes == null) {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700479 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800480 return holder.fresh;
481 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800482
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800483 // Optionally decode bytes into a bitmap
484 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800485
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800486 view.setImageBitmap(holder.bitmap);
487
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700488 if (holder.bitmap != null) {
489 // Put the bitmap in the LRU cache
490 mBitmapCache.put(request, holder.bitmap);
491 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800492
493 // Soften the reference
494 holder.bitmap = null;
495
496 return holder.fresh;
497 }
498
499 /**
500 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
501 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
502 * the holder, it will not be necessary to decode the bitmap.
503 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800504 private static void inflateBitmap(BitmapHolder holder) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800505 byte[] bytes = holder.bytes;
506 if (bytes == null || bytes.length == 0) {
507 return;
508 }
509
510 // Check the soft reference. If will be retained if the bitmap is also
511 // in the LRU cache, so we don't need to check the LRU cache explicitly.
512 if (holder.bitmapRef != null) {
513 holder.bitmap = holder.bitmapRef.get();
514 if (holder.bitmap != null) {
515 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800516 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800517 }
518
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800519 try {
520 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
521 holder.bitmap = bitmap;
522 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800523 if (DEBUG) {
524 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
525 + bitmap.getWidth() + "x" + bitmap.getHeight()
526 + ", " + btk(bitmap.getByteCount()));
527 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800528 } catch (OutOfMemoryError e) {
529 // Do nothing - the photo will appear to be missing
530 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800531 }
532
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700533 public void clear() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800534 if (DEBUG) Log.d(TAG, "clear");
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700535 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800536 mBitmapHolderCache.evictAll();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800537 mBitmapCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700538 }
539
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800540 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800541 public void pause() {
542 mPaused = true;
543 }
544
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800545 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800546 public void resume() {
547 mPaused = false;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800548 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800549 if (!mPendingRequests.isEmpty()) {
550 requestLoading();
551 }
552 }
553
554 /**
555 * Sends a message to this thread itself to start loading images. If the current
556 * view contains multiple image views, all of those image views will get a chance
557 * to request their respective photos before any of those requests are executed.
558 * This allows us to load images in bulk.
559 */
560 private void requestLoading() {
561 if (!mLoadingRequested) {
562 mLoadingRequested = true;
563 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
564 }
565 }
566
567 /**
568 * Processes requests on the main thread.
569 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700570 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800571 public boolean handleMessage(Message msg) {
572 switch (msg.what) {
573 case MESSAGE_REQUEST_LOADING: {
574 mLoadingRequested = false;
575 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800576 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800577 mLoaderThread.requestLoading();
578 }
579 return true;
580 }
581
582 case MESSAGE_PHOTOS_LOADED: {
583 if (!mPaused) {
584 processLoadedImages();
585 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800586 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800587 return true;
588 }
589 }
590 return false;
591 }
592
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800593 public void ensureLoaderThread() {
594 if (mLoaderThread == null) {
595 mLoaderThread = new LoaderThread(mContext.getContentResolver());
596 mLoaderThread.start();
597 }
598 }
599
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800600 /**
601 * Goes over pending loading requests and displays loaded photos. If some of the
602 * photos still haven't been loaded, sends another request for image loading.
603 */
604 private void processLoadedImages() {
605 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
606 while (iterator.hasNext()) {
607 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700608 Request key = mPendingRequests.get(view);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700609 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800610 if (loaded) {
611 iterator.remove();
612 }
613 }
614
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800615 softenCache();
616
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800617 if (!mPendingRequests.isEmpty()) {
618 requestLoading();
619 }
620 }
621
622 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800623 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800624 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800625 */
626 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800627 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800628 holder.bitmap = null;
629 }
630 }
631
632 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800633 * Stores the supplied bitmap in cache.
634 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800635 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800636 if (DEBUG) {
637 BitmapHolder prev = mBitmapHolderCache.get(key);
638 if (prev != null && prev.bytes != null) {
639 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
640 if (prev.fresh) {
641 mFreshCacheOverwrite.incrementAndGet();
642 } else {
643 mStaleCacheOverwrite.incrementAndGet();
644 }
645 }
646 Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
647 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800648 BitmapHolder holder = new BitmapHolder(bytes);
649 holder.fresh = true;
650
651 // Unless this image is being preloaded, decode it right away while
652 // we are still on the background thread.
653 if (!preloading) {
654 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800655 }
656
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800657 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800658 }
659
Dave Santoro14d20832011-12-02 18:14:52 -0800660 @Override
661 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
662 Request request = Request.createFromUri(photoUri, true, false, DEFAULT_AVATER);
663 BitmapHolder holder = new BitmapHolder(photoBytes);
664 mBitmapHolderCache.put(request.getKey(), holder);
665 mBitmapCache.put(request, bitmap);
666 }
667
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800668 /**
669 * Populates an array of photo IDs that need to be loaded.
670 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100671 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
672 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800673 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800674 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700675 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800676
677 /*
678 * Since the call is made from the loader thread, the map could be
679 * changing during the iteration. That's not really a problem:
680 * ConcurrentHashMap will allow those changes to happen without throwing
681 * exceptions. Since we may miss some requests in the situation of
682 * concurrent change, we will need to check the map again once loading
683 * is complete.
684 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700685 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800686 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700687 Request request = iterator.next();
688 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800689 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700690 if (request.isUriRequest()) {
691 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700692 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700693 photoIds.add(request.mId);
694 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700695 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800696 }
697 }
698 }
699
700 /**
701 * The thread that performs loading of photos from the database.
702 */
703 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700704 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800705 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
706 private static final int MESSAGE_LOAD_PHOTOS = 1;
707
708 /**
709 * A pause between preload batches that yields to the UI thread.
710 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700711 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800712
713 /**
714 * Number of photos to preload per batch.
715 */
716 private static final int PRELOAD_BATCH = 25;
717
718 /**
719 * Maximum number of photos to preload. If the cache size is 2Mb and
720 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
721 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700722 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700723
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800724 private final ContentResolver mResolver;
725 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100726 private final Set<Long> mPhotoIds = Sets.newHashSet();
727 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
728 private final Set<Uri> mPhotoUris = Sets.newHashSet();
729 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800730
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800731 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700732 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800733
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800734 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
735 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
736 private static final int PRELOAD_STATUS_DONE = 2;
737
738 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
739
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800740 public LoaderThread(ContentResolver resolver) {
741 super(LOADER_THREAD_NAME);
742 mResolver = resolver;
743 }
744
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800745 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800746 if (mLoaderThreadHandler == null) {
747 mLoaderThreadHandler = new Handler(getLooper(), this);
748 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800749 }
750
751 /**
752 * Kicks off preloading of the next batch of photos on the background thread.
753 * Preloading will happen after a delay: we want to yield to the UI thread
754 * as much as possible.
755 * <p>
756 * If preloading is already complete, does nothing.
757 */
758 public void requestPreloading() {
759 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
760 return;
761 }
762
763 ensureHandler();
764 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
765 return;
766 }
767
768 mLoaderThreadHandler.sendEmptyMessageDelayed(
769 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
770 }
771
772 /**
773 * Sends a message to this thread to load requested photos. Cancels a preloading
774 * request, if any: we don't want preloading to impede loading of the photos
775 * we need to display now.
776 */
777 public void requestLoading() {
778 ensureHandler();
779 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
780 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800781 }
782
783 /**
784 * Receives the above message, loads photos and then sends a message
785 * to the main thread to process them.
786 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700787 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800788 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800789 switch (msg.what) {
790 case MESSAGE_PRELOAD_PHOTOS:
791 preloadPhotosInBackground();
792 break;
793 case MESSAGE_LOAD_PHOTOS:
794 loadPhotosInBackground();
795 break;
796 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800797 return true;
798 }
799
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800800 /**
801 * The first time it is called, figures out which photos need to be preloaded.
802 * Each subsequent call preloads the next batch of photos and requests
803 * another cycle of preloading after a delay. The whole process ends when
804 * we either run out of photos to preload or fill up cache.
805 */
806 private void preloadPhotosInBackground() {
807 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
808 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800809 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800810
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800811 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
812 queryPhotosForPreload();
813 if (mPreloadPhotoIds.isEmpty()) {
814 mPreloadStatus = PRELOAD_STATUS_DONE;
815 } else {
816 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
817 }
818 requestPreloading();
819 return;
820 }
821
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800822 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800823 mPreloadStatus = PRELOAD_STATUS_DONE;
824 return;
825 }
826
827 mPhotoIds.clear();
828 mPhotoIdsAsStrings.clear();
829
830 int count = 0;
831 int preloadSize = mPreloadPhotoIds.size();
832 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
833 preloadSize--;
834 count++;
835 Long photoId = mPreloadPhotoIds.get(preloadSize);
836 mPhotoIds.add(photoId);
837 mPhotoIdsAsStrings.add(photoId.toString());
838 mPreloadPhotoIds.remove(preloadSize);
839 }
840
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700841 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800842
843 if (preloadSize == 0) {
844 mPreloadStatus = PRELOAD_STATUS_DONE;
845 }
846
Makoto Onuki173f2812011-09-06 14:49:27 -0700847 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
848 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800849
850 requestPreloading();
851 }
852
853 private void queryPhotosForPreload() {
854 Cursor cursor = null;
855 try {
856 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
857 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700858 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
859 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800860 .build();
861 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
862 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
863 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700864 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800865
866 if (cursor != null) {
867 while (cursor.moveToNext()) {
868 // Insert them in reverse order, because we will be taking
869 // them from the end of the list for loading.
870 mPreloadPhotoIds.add(0, cursor.getLong(0));
871 }
872 }
873 } finally {
874 if (cursor != null) {
875 cursor.close();
876 }
877 }
878 }
879
880 private void loadPhotosInBackground() {
881 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700882 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800883 loadRemotePhotos();
884 requestPreloading();
885 }
886
887 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100888 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800889 return;
890 }
891
892 // Remove loaded photos from the preload queue: we don't want
893 // the preloading process to load them again.
894 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100895 for (Long id : mPhotoIds) {
896 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800897 }
898 if (mPreloadPhotoIds.isEmpty()) {
899 mPreloadStatus = PRELOAD_STATUS_DONE;
900 }
901 }
902
903 mStringBuilder.setLength(0);
904 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100905 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800906 if (i != 0) {
907 mStringBuilder.append(',');
908 }
909 mStringBuilder.append('?');
910 }
911 mStringBuilder.append(')');
912
913 Cursor cursor = null;
914 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800915 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
Dave Santoro84cac442011-08-24 15:23:10 -0700916 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800917 COLUMNS,
918 mStringBuilder.toString(),
919 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
920 null);
921
922 if (cursor != null) {
923 while (cursor.moveToNext()) {
924 Long id = cursor.getLong(0);
925 byte[] bytes = cursor.getBlob(1);
926 cacheBitmap(id, bytes, preloading);
927 mPhotoIds.remove(id);
928 }
929 }
930 } finally {
931 if (cursor != null) {
932 cursor.close();
933 }
934 }
935
Dave Santoro84cac442011-08-24 15:23:10 -0700936 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100937 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700938 if (ContactsContract.isProfileId(id)) {
939 Cursor profileCursor = null;
940 try {
941 profileCursor = mResolver.query(
942 ContentUris.withAppendedId(Data.CONTENT_URI, id),
943 COLUMNS, null, null, null);
944 if (profileCursor != null && profileCursor.moveToFirst()) {
945 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
946 preloading);
947 } else {
948 // Couldn't load a photo this way either.
949 cacheBitmap(id, null, preloading);
950 }
951 } finally {
952 if (profileCursor != null) {
953 profileCursor.close();
954 }
955 }
956 } else {
957 // Not a profile photo and not found - mark the cache accordingly
958 cacheBitmap(id, null, preloading);
959 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800960 }
961
962 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
963 }
964
965 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100966 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700967 if (mBuffer == null) {
968 mBuffer = new byte[BUFFER_SIZE];
969 }
970 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800971 if (DEBUG) Log.d(TAG, "Loading " + uri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700972 InputStream is = mResolver.openInputStream(uri);
973 if (is != null) {
974 ByteArrayOutputStream baos = new ByteArrayOutputStream();
975 try {
976 int size;
977 while ((size = is.read(mBuffer)) != -1) {
978 baos.write(mBuffer, 0, size);
979 }
980 } finally {
981 is.close();
982 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800983 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700984 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700985 } else {
986 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800987 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700988 }
989 } catch (Exception ex) {
990 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800991 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700992 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800993 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800994 }
995 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700996
997 /**
998 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
999 * light theme
1000 */
1001 private static final class Request {
1002 private final long mId;
1003 private final Uri mUri;
1004 private final boolean mDarkTheme;
1005 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001006 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001007
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001008 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
1009 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001010 mId = id;
1011 mUri = uri;
1012 mDarkTheme = darkTheme;
1013 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001014 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001015 }
1016
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001017 public static Request createFromId(long id, boolean hires, boolean darkTheme,
1018 DefaultImageProvider defaultProvider) {
1019 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001020 }
1021
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001022 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
1023 DefaultImageProvider defaultProvider) {
1024 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001025 }
1026
1027 public boolean isDarkTheme() {
1028 return mDarkTheme;
1029 }
1030
1031 public boolean isHires() {
1032 return mHires;
1033 }
1034
1035 public boolean isUriRequest() {
1036 return mUri != null;
1037 }
1038
1039 @Override
1040 public int hashCode() {
1041 if (mUri != null) return mUri.hashCode();
1042
1043 // copied over from Long.hashCode()
1044 return (int) (mId ^ (mId >>> 32));
1045 }
1046
1047 @Override
1048 public boolean equals(Object o) {
1049 if (!(o instanceof Request)) return false;
1050 final Request that = (Request) o;
1051 // Don't compare equality of mHires and mDarkTheme fields because these are only used
1052 // in the default contact photo case. When the contact does have a photo, the contact
1053 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
1054 // the photo request on the queue twice.
1055 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
1056 }
1057
1058 public Object getKey() {
1059 return mUri == null ? mId : mUri;
1060 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001061
1062 public void applyDefaultImage(ImageView view) {
1063 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
1064 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001065 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001066}