blob: 7c54e8069e17c0c759597374c198fa648cb6323c [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 /**
180 * Initiates a background process that over time will fill up cache with
181 * preload photos.
182 */
183 public abstract void preloadPhotosInBackground();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800184
185 // ComponentCallbacks2
186 @Override
187 public void onConfigurationChanged(Configuration newConfig) {
188 }
189
190 // ComponentCallbacks2
191 @Override
192 public void onLowMemory() {
193 }
194
195 // ComponentCallbacks2
196 @Override
197 public void onTrimMemory(int level) {
198 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800199}
200
201class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800202 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
203
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800204 /**
205 * Type of message sent by the UI thread to itself to indicate that some photos
206 * need to be loaded.
207 */
208 private static final int MESSAGE_REQUEST_LOADING = 1;
209
210 /**
211 * Type of message sent by the loader thread to indicate that some photos have
212 * been loaded.
213 */
214 private static final int MESSAGE_PHOTOS_LOADED = 2;
215
216 private static final String[] EMPTY_STRING_ARRAY = new String[0];
217
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800218 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800219
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800220 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800221 * Maintains the state of a particular photo.
222 */
223 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800224 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800225
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800226 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800227 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700228 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800229
230 public BitmapHolder(byte[] bytes) {
231 this.bytes = bytes;
232 this.fresh = true;
233 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800234 }
235
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800236 private final Context mContext;
237
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800238 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800239 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800240 * as they come from the database. Each holder has a soft reference to the
241 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800242 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800243 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
244
245 /**
246 * Cache size threshold at which bitmaps will not be preloaded.
247 */
248 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800249
250 /**
251 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
252 * the most recently used bitmaps to save time on decoding
253 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
254 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800255 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800256
257 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700258 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
259 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800260 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700261 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
262 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800263
264 /**
265 * Handler for messages sent to the UI thread.
266 */
267 private final Handler mMainThreadHandler = new Handler(this);
268
269 /**
270 * Thread responsible for loading photos from the database. Created upon
271 * the first request.
272 */
273 private LoaderThread mLoaderThread;
274
275 /**
276 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
277 */
278 private boolean mLoadingRequested;
279
280 /**
281 * Flag indicating if the image loading is paused.
282 */
283 private boolean mPaused;
284
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800285 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
286 private static final int HOLDER_CACHE_SIZE = 2000000;
287
288 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
289 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
290
291 private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
292
293 /** For debug: How many times we had to reload cached photo for a stale entry */
294 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
295
296 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
297 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
298
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800299 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800300 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800301
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800302 final float cacheSizeAdjustment =
303 (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
304 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
305 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
306 @Override protected int sizeOf(Object key, Bitmap value) {
307 return value.getByteCount();
308 }
309
310 @Override protected void entryRemoved(
311 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
312 if (DEBUG) dumpStats();
313 }
314 };
315 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
316 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800317 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800318 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800319 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800320
321 @Override protected void entryRemoved(
322 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
323 if (DEBUG) dumpStats();
324 }
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800325 };
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800326 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
327 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
328 if (DEBUG) {
329 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
330 + " + " + btk(mBitmapCache.maxSize()));
331 }
332 }
333
334 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
335 private static String btk(int bytes) {
336 return ((bytes + 1023) / 1024) + "K";
337 }
338
339 private static final int safeDiv(int dividend, int divisor) {
340 return (divisor == 0) ? 0 : (dividend / divisor);
341 }
342
343 /**
344 * Dump cache stats on logcat.
345 */
346 private void dumpStats() {
347 if (!DEBUG) return;
348 {
349 int numHolders = 0;
350 int rawBytes = 0;
351 int bitmapBytes = 0;
352 int numBitmaps = 0;
353 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
354 numHolders++;
355 if (h.bytes != null) {
356 rawBytes += h.bytes.length;
357 }
358 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
359 if (b != null) {
360 numBitmaps++;
361 bitmapBytes += b.getByteCount();
362 }
363 }
364 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
365 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
366 + numBitmaps + " bitmaps, avg: "
367 + btk(safeDiv(rawBytes, numHolders))
368 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
369 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
370 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
371 + " stale=" + mStaleCacheOverwrite.get());
372 }
373
374 {
375 int numBitmaps = 0;
376 int bitmapBytes = 0;
377 for (Bitmap b : mBitmapCache.snapshot().values()) {
378 numBitmaps++;
379 bitmapBytes += b.getByteCount();
380 }
381 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
382 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
383 // We don't get from L2 cache, so L2 stats is meaningless.
384 }
385 }
386
387 @Override
388 public void onTrimMemory(int level) {
389 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
390 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
391 // Clear the caches. Note all pending requests will be removed too.
392 clear();
393 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800394 }
395
396 @Override
397 public void preloadPhotosInBackground() {
398 ensureLoaderThread();
399 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800400 }
401
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800402 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700403 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
404 DefaultImageProvider defaultProvider) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800405 if (photoId == 0) {
406 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700407 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800408 mPendingRequests.remove(view);
409 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800410 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700411 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
412 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700413 }
414 }
415
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800416 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700417 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
418 DefaultImageProvider defaultProvider) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700419 if (photoUri == null) {
420 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700421 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700422 mPendingRequests.remove(view);
423 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800424 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700425 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
426 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700427 }
428 }
429
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700430 private void loadPhotoByIdOrUri(ImageView view, Request request) {
431 boolean loaded = loadCachedPhoto(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700432 if (loaded) {
433 mPendingRequests.remove(view);
434 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700435 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700436 if (!mPaused) {
437 // Send a request to start loading photos
438 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800439 }
440 }
441 }
442
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800443 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700444 public void removePhoto(ImageView view) {
445 view.setImageDrawable(null);
446 mPendingRequests.remove(view);
447 }
448
449 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800450 public void refreshCache() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800451 if (DEBUG) Log.d(TAG, "refreshCache");
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800452 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800453 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800454 }
455 }
456
457 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800458 * Checks if the photo is present in cache. If so, sets the photo on the view.
459 *
460 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800461 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700462 private boolean loadCachedPhoto(ImageView view, Request request) {
463 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800464 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800465 // The bitmap has not been loaded - should display the placeholder image.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700466 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800467 return false;
468 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800469
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800470 if (holder.bytes == null) {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700471 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800472 return holder.fresh;
473 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800474
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800475 // Optionally decode bytes into a bitmap
476 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800477
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800478 view.setImageBitmap(holder.bitmap);
479
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700480 if (holder.bitmap != null) {
481 // Put the bitmap in the LRU cache
482 mBitmapCache.put(request, holder.bitmap);
483 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800484
485 // Soften the reference
486 holder.bitmap = null;
487
488 return holder.fresh;
489 }
490
491 /**
492 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
493 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
494 * the holder, it will not be necessary to decode the bitmap.
495 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800496 private static void inflateBitmap(BitmapHolder holder) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800497 byte[] bytes = holder.bytes;
498 if (bytes == null || bytes.length == 0) {
499 return;
500 }
501
502 // Check the soft reference. If will be retained if the bitmap is also
503 // in the LRU cache, so we don't need to check the LRU cache explicitly.
504 if (holder.bitmapRef != null) {
505 holder.bitmap = holder.bitmapRef.get();
506 if (holder.bitmap != null) {
507 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800508 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800509 }
510
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800511 try {
512 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
513 holder.bitmap = bitmap;
514 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800515 if (DEBUG) {
516 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
517 + bitmap.getWidth() + "x" + bitmap.getHeight()
518 + ", " + btk(bitmap.getByteCount()));
519 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800520 } catch (OutOfMemoryError e) {
521 // Do nothing - the photo will appear to be missing
522 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800523 }
524
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700525 public void clear() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800526 if (DEBUG) Log.d(TAG, "clear");
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700527 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800528 mBitmapHolderCache.evictAll();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800529 mBitmapCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700530 }
531
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800532 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800533 public void pause() {
534 mPaused = true;
535 }
536
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800537 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800538 public void resume() {
539 mPaused = false;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800540 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800541 if (!mPendingRequests.isEmpty()) {
542 requestLoading();
543 }
544 }
545
546 /**
547 * Sends a message to this thread itself to start loading images. If the current
548 * view contains multiple image views, all of those image views will get a chance
549 * to request their respective photos before any of those requests are executed.
550 * This allows us to load images in bulk.
551 */
552 private void requestLoading() {
553 if (!mLoadingRequested) {
554 mLoadingRequested = true;
555 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
556 }
557 }
558
559 /**
560 * Processes requests on the main thread.
561 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700562 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800563 public boolean handleMessage(Message msg) {
564 switch (msg.what) {
565 case MESSAGE_REQUEST_LOADING: {
566 mLoadingRequested = false;
567 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800568 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800569 mLoaderThread.requestLoading();
570 }
571 return true;
572 }
573
574 case MESSAGE_PHOTOS_LOADED: {
575 if (!mPaused) {
576 processLoadedImages();
577 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800578 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800579 return true;
580 }
581 }
582 return false;
583 }
584
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800585 public void ensureLoaderThread() {
586 if (mLoaderThread == null) {
587 mLoaderThread = new LoaderThread(mContext.getContentResolver());
588 mLoaderThread.start();
589 }
590 }
591
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800592 /**
593 * Goes over pending loading requests and displays loaded photos. If some of the
594 * photos still haven't been loaded, sends another request for image loading.
595 */
596 private void processLoadedImages() {
597 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
598 while (iterator.hasNext()) {
599 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700600 Request key = mPendingRequests.get(view);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700601 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800602 if (loaded) {
603 iterator.remove();
604 }
605 }
606
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800607 softenCache();
608
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800609 if (!mPendingRequests.isEmpty()) {
610 requestLoading();
611 }
612 }
613
614 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800615 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800616 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800617 */
618 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800619 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800620 holder.bitmap = null;
621 }
622 }
623
624 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800625 * Stores the supplied bitmap in cache.
626 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800627 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800628 if (DEBUG) {
629 BitmapHolder prev = mBitmapHolderCache.get(key);
630 if (prev != null && prev.bytes != null) {
631 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
632 if (prev.fresh) {
633 mFreshCacheOverwrite.incrementAndGet();
634 } else {
635 mStaleCacheOverwrite.incrementAndGet();
636 }
637 }
638 Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
639 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800640 BitmapHolder holder = new BitmapHolder(bytes);
641 holder.fresh = true;
642
643 // Unless this image is being preloaded, decode it right away while
644 // we are still on the background thread.
645 if (!preloading) {
646 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800647 }
648
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800649 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800650 }
651
652 /**
653 * Populates an array of photo IDs that need to be loaded.
654 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100655 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
656 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800657 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800658 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700659 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800660
661 /*
662 * Since the call is made from the loader thread, the map could be
663 * changing during the iteration. That's not really a problem:
664 * ConcurrentHashMap will allow those changes to happen without throwing
665 * exceptions. Since we may miss some requests in the situation of
666 * concurrent change, we will need to check the map again once loading
667 * is complete.
668 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700669 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800670 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700671 Request request = iterator.next();
672 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800673 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700674 if (request.isUriRequest()) {
675 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700676 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700677 photoIds.add(request.mId);
678 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700679 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800680 }
681 }
682 }
683
684 /**
685 * The thread that performs loading of photos from the database.
686 */
687 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700688 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800689 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
690 private static final int MESSAGE_LOAD_PHOTOS = 1;
691
692 /**
693 * A pause between preload batches that yields to the UI thread.
694 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700695 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800696
697 /**
698 * Number of photos to preload per batch.
699 */
700 private static final int PRELOAD_BATCH = 25;
701
702 /**
703 * Maximum number of photos to preload. If the cache size is 2Mb and
704 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
705 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700706 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700707
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800708 private final ContentResolver mResolver;
709 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100710 private final Set<Long> mPhotoIds = Sets.newHashSet();
711 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
712 private final Set<Uri> mPhotoUris = Sets.newHashSet();
713 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800714
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800715 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700716 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800717
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800718 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
719 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
720 private static final int PRELOAD_STATUS_DONE = 2;
721
722 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
723
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800724 public LoaderThread(ContentResolver resolver) {
725 super(LOADER_THREAD_NAME);
726 mResolver = resolver;
727 }
728
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800729 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800730 if (mLoaderThreadHandler == null) {
731 mLoaderThreadHandler = new Handler(getLooper(), this);
732 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800733 }
734
735 /**
736 * Kicks off preloading of the next batch of photos on the background thread.
737 * Preloading will happen after a delay: we want to yield to the UI thread
738 * as much as possible.
739 * <p>
740 * If preloading is already complete, does nothing.
741 */
742 public void requestPreloading() {
743 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
744 return;
745 }
746
747 ensureHandler();
748 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
749 return;
750 }
751
752 mLoaderThreadHandler.sendEmptyMessageDelayed(
753 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
754 }
755
756 /**
757 * Sends a message to this thread to load requested photos. Cancels a preloading
758 * request, if any: we don't want preloading to impede loading of the photos
759 * we need to display now.
760 */
761 public void requestLoading() {
762 ensureHandler();
763 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
764 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800765 }
766
767 /**
768 * Receives the above message, loads photos and then sends a message
769 * to the main thread to process them.
770 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700771 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800772 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800773 switch (msg.what) {
774 case MESSAGE_PRELOAD_PHOTOS:
775 preloadPhotosInBackground();
776 break;
777 case MESSAGE_LOAD_PHOTOS:
778 loadPhotosInBackground();
779 break;
780 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800781 return true;
782 }
783
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800784 /**
785 * The first time it is called, figures out which photos need to be preloaded.
786 * Each subsequent call preloads the next batch of photos and requests
787 * another cycle of preloading after a delay. The whole process ends when
788 * we either run out of photos to preload or fill up cache.
789 */
790 private void preloadPhotosInBackground() {
791 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
792 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800793 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800794
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800795 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
796 queryPhotosForPreload();
797 if (mPreloadPhotoIds.isEmpty()) {
798 mPreloadStatus = PRELOAD_STATUS_DONE;
799 } else {
800 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
801 }
802 requestPreloading();
803 return;
804 }
805
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800806 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800807 mPreloadStatus = PRELOAD_STATUS_DONE;
808 return;
809 }
810
811 mPhotoIds.clear();
812 mPhotoIdsAsStrings.clear();
813
814 int count = 0;
815 int preloadSize = mPreloadPhotoIds.size();
816 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
817 preloadSize--;
818 count++;
819 Long photoId = mPreloadPhotoIds.get(preloadSize);
820 mPhotoIds.add(photoId);
821 mPhotoIdsAsStrings.add(photoId.toString());
822 mPreloadPhotoIds.remove(preloadSize);
823 }
824
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700825 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800826
827 if (preloadSize == 0) {
828 mPreloadStatus = PRELOAD_STATUS_DONE;
829 }
830
Makoto Onuki173f2812011-09-06 14:49:27 -0700831 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
832 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800833
834 requestPreloading();
835 }
836
837 private void queryPhotosForPreload() {
838 Cursor cursor = null;
839 try {
840 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
841 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700842 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
843 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800844 .build();
845 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
846 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
847 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700848 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800849
850 if (cursor != null) {
851 while (cursor.moveToNext()) {
852 // Insert them in reverse order, because we will be taking
853 // them from the end of the list for loading.
854 mPreloadPhotoIds.add(0, cursor.getLong(0));
855 }
856 }
857 } finally {
858 if (cursor != null) {
859 cursor.close();
860 }
861 }
862 }
863
864 private void loadPhotosInBackground() {
865 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700866 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800867 loadRemotePhotos();
868 requestPreloading();
869 }
870
871 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100872 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800873 return;
874 }
875
876 // Remove loaded photos from the preload queue: we don't want
877 // the preloading process to load them again.
878 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100879 for (Long id : mPhotoIds) {
880 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800881 }
882 if (mPreloadPhotoIds.isEmpty()) {
883 mPreloadStatus = PRELOAD_STATUS_DONE;
884 }
885 }
886
887 mStringBuilder.setLength(0);
888 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100889 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800890 if (i != 0) {
891 mStringBuilder.append(',');
892 }
893 mStringBuilder.append('?');
894 }
895 mStringBuilder.append(')');
896
897 Cursor cursor = null;
898 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800899 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
Dave Santoro84cac442011-08-24 15:23:10 -0700900 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800901 COLUMNS,
902 mStringBuilder.toString(),
903 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
904 null);
905
906 if (cursor != null) {
907 while (cursor.moveToNext()) {
908 Long id = cursor.getLong(0);
909 byte[] bytes = cursor.getBlob(1);
910 cacheBitmap(id, bytes, preloading);
911 mPhotoIds.remove(id);
912 }
913 }
914 } finally {
915 if (cursor != null) {
916 cursor.close();
917 }
918 }
919
Dave Santoro84cac442011-08-24 15:23:10 -0700920 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100921 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700922 if (ContactsContract.isProfileId(id)) {
923 Cursor profileCursor = null;
924 try {
925 profileCursor = mResolver.query(
926 ContentUris.withAppendedId(Data.CONTENT_URI, id),
927 COLUMNS, null, null, null);
928 if (profileCursor != null && profileCursor.moveToFirst()) {
929 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
930 preloading);
931 } else {
932 // Couldn't load a photo this way either.
933 cacheBitmap(id, null, preloading);
934 }
935 } finally {
936 if (profileCursor != null) {
937 profileCursor.close();
938 }
939 }
940 } else {
941 // Not a profile photo and not found - mark the cache accordingly
942 cacheBitmap(id, null, preloading);
943 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800944 }
945
946 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
947 }
948
949 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100950 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700951 if (mBuffer == null) {
952 mBuffer = new byte[BUFFER_SIZE];
953 }
954 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800955 if (DEBUG) Log.d(TAG, "Loading " + uri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700956 InputStream is = mResolver.openInputStream(uri);
957 if (is != null) {
958 ByteArrayOutputStream baos = new ByteArrayOutputStream();
959 try {
960 int size;
961 while ((size = is.read(mBuffer)) != -1) {
962 baos.write(mBuffer, 0, size);
963 }
964 } finally {
965 is.close();
966 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800967 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700968 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700969 } else {
970 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800971 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700972 }
973 } catch (Exception ex) {
974 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800975 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700976 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800977 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800978 }
979 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700980
981 /**
982 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
983 * light theme
984 */
985 private static final class Request {
986 private final long mId;
987 private final Uri mUri;
988 private final boolean mDarkTheme;
989 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700990 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700991
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700992 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
993 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700994 mId = id;
995 mUri = uri;
996 mDarkTheme = darkTheme;
997 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700998 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700999 }
1000
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001001 public static Request createFromId(long id, boolean hires, boolean darkTheme,
1002 DefaultImageProvider defaultProvider) {
1003 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001004 }
1005
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001006 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
1007 DefaultImageProvider defaultProvider) {
1008 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001009 }
1010
1011 public boolean isDarkTheme() {
1012 return mDarkTheme;
1013 }
1014
1015 public boolean isHires() {
1016 return mHires;
1017 }
1018
1019 public boolean isUriRequest() {
1020 return mUri != null;
1021 }
1022
1023 @Override
1024 public int hashCode() {
1025 if (mUri != null) return mUri.hashCode();
1026
1027 // copied over from Long.hashCode()
1028 return (int) (mId ^ (mId >>> 32));
1029 }
1030
1031 @Override
1032 public boolean equals(Object o) {
1033 if (!(o instanceof Request)) return false;
1034 final Request that = (Request) o;
1035 // Don't compare equality of mHires and mDarkTheme fields because these are only used
1036 // in the default contact photo case. When the contact does have a photo, the contact
1037 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
1038 // the photo request on the queue twice.
1039 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
1040 }
1041
1042 public Object getKey() {
1043 return mUri == null ? mId : mUri;
1044 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001045
1046 public void applyDefaultImage(ImageView view) {
1047 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
1048 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001049 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001050}