blob: 0f7065de78c29dabac3317409b023d222c6e3a84 [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;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080020import com.google.android.collect.Lists;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010021import com.google.android.collect.Sets;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080022
23import android.content.ContentResolver;
24import android.content.Context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080025import android.content.res.Resources;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080026import android.database.Cursor;
27import android.graphics.Bitmap;
28import android.graphics.BitmapFactory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070029import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080030import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070031import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080032import android.os.HandlerThread;
33import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080034import android.provider.ContactsContract;
35import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080036import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070037import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080038import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070039import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080040import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080041import android.widget.ImageView;
42
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070043import java.io.ByteArrayOutputStream;
44import java.io.InputStream;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080045import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080046import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010047import java.util.List;
48import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080049import java.util.concurrent.ConcurrentHashMap;
50
51/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080052 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080053 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080054public abstract class ContactPhotoManager {
55
56 static final String TAG = "ContactPhotoManager";
57
58 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
59
60 /**
61 * The resource ID of the image to be used when the photo is unavailable or being
62 * loaded.
63 */
64 protected final int mDefaultResourceId = R.drawable.ic_contact_picture;
65
66 /**
67 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
68 * the available authenticators. This method can safely be called from the UI thread.
69 */
70 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010071 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080072 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +010073 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080074 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010075 service = createContactPhotoManager(applicationContext);
76 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080077 }
78 return service;
79 }
80
81 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
82 return new ContactPhotoManagerImpl(context);
83 }
84
85 /**
86 * Load photo into the supplied image view. If the photo is already cached,
87 * it is displayed immediately. Otherwise a request is sent to load the photo
88 * from the database.
89 */
90 public abstract void loadPhoto(ImageView view, long photoId);
91
92 /**
93 * Load photo into the supplied image view. If the photo is already cached,
94 * it is displayed immediately. Otherwise a request is sent to load the photo
95 * from the location specified by the URI.
96 */
97 public abstract void loadPhoto(ImageView view, Uri photoUri);
98
99 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700100 * Remove photo from the supplied image view. This also cancels current pending load request
101 * inside this photo manager.
102 */
103 public abstract void removePhoto(ImageView view);
104
105 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800106 * Temporarily stops loading photos from the database.
107 */
108 public abstract void pause();
109
110 /**
111 * Resumes loading photos from the database.
112 */
113 public abstract void resume();
114
115 /**
116 * Marks all cached photos for reloading. We can continue using cache but should
117 * also make sure the photos haven't changed in the background and notify the views
118 * if so.
119 */
120 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800121
122 /**
123 * Initiates a background process that over time will fill up cache with
124 * preload photos.
125 */
126 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800127}
128
129class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800130 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
131
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800132 /**
133 * Type of message sent by the UI thread to itself to indicate that some photos
134 * need to be loaded.
135 */
136 private static final int MESSAGE_REQUEST_LOADING = 1;
137
138 /**
139 * Type of message sent by the loader thread to indicate that some photos have
140 * been loaded.
141 */
142 private static final int MESSAGE_PHOTOS_LOADED = 2;
143
144 private static final String[] EMPTY_STRING_ARRAY = new String[0];
145
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800146 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800147
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800148 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800149 * Maintains the state of a particular photo.
150 */
151 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800152 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800153
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800154 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800155 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800156 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800157
158 public BitmapHolder(byte[] bytes) {
159 this.bytes = bytes;
160 this.fresh = true;
161 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800162 }
163
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800164 private final Context mContext;
165
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800166 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800167 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800168 * as they come from the database. Each holder has a soft reference to the
169 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800170 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800171 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
172
173 /**
174 * Cache size threshold at which bitmaps will not be preloaded.
175 */
176 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800177
178 /**
179 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
180 * the most recently used bitmaps to save time on decoding
181 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
182 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800183 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800184
185 /**
186 * A map from ImageView to the corresponding photo ID. Please note that this
187 * photo ID may change before the photo loading request is started.
188 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700189 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
190 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800191
192 /**
193 * Handler for messages sent to the UI thread.
194 */
195 private final Handler mMainThreadHandler = new Handler(this);
196
197 /**
198 * Thread responsible for loading photos from the database. Created upon
199 * the first request.
200 */
201 private LoaderThread mLoaderThread;
202
203 /**
204 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
205 */
206 private boolean mLoadingRequested;
207
208 /**
209 * Flag indicating if the image loading is paused.
210 */
211 private boolean mPaused;
212
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800213 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800214 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800215
216 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800217 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800218 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800219 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
220 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
221 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800222 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800223 }
224 };
225 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800226 }
227
228 @Override
229 public void preloadPhotosInBackground() {
230 ensureLoaderThread();
231 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800232 }
233
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800234 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800235 public void loadPhoto(ImageView view, long photoId) {
236 if (photoId == 0) {
237 // No photo is needed
238 view.setImageResource(mDefaultResourceId);
239 mPendingRequests.remove(view);
240 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700241 loadPhotoByIdOrUri(view, photoId);
242 }
243 }
244
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800245 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700246 public void loadPhoto(ImageView view, Uri photoUri) {
247 if (photoUri == null) {
248 // No photo is needed
249 view.setImageResource(mDefaultResourceId);
250 mPendingRequests.remove(view);
251 } else {
252 loadPhotoByIdOrUri(view, photoUri);
253 }
254 }
255
256 private void loadPhotoByIdOrUri(ImageView view, Object key) {
257 boolean loaded = loadCachedPhoto(view, key);
258 if (loaded) {
259 mPendingRequests.remove(view);
260 } else {
261 mPendingRequests.put(view, key);
262 if (!mPaused) {
263 // Send a request to start loading photos
264 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800265 }
266 }
267 }
268
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800269 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700270 public void removePhoto(ImageView view) {
271 view.setImageDrawable(null);
272 mPendingRequests.remove(view);
273 }
274
275 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800276 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800277 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800278 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800279 }
280 }
281
282 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800283 * Checks if the photo is present in cache. If so, sets the photo on the view.
284 *
285 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800286 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700287 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800288 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800289 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800290 // The bitmap has not been loaded - should display the placeholder image.
291 view.setImageResource(mDefaultResourceId);
292 return false;
293 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800294
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800295 if (holder.bytes == null) {
296 view.setImageResource(mDefaultResourceId);
297 return holder.fresh;
298 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800299
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800300 // Optionally decode bytes into a bitmap
301 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800302
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800303 view.setImageBitmap(holder.bitmap);
304
305 // Put the bitmap in the LRU cache
306 mBitmapCache.put(key, holder.bitmap);
307
308 // Soften the reference
309 holder.bitmap = null;
310
311 return holder.fresh;
312 }
313
314 /**
315 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
316 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
317 * the holder, it will not be necessary to decode the bitmap.
318 */
319 private void inflateBitmap(BitmapHolder holder) {
320 byte[] bytes = holder.bytes;
321 if (bytes == null || bytes.length == 0) {
322 return;
323 }
324
325 // Check the soft reference. If will be retained if the bitmap is also
326 // in the LRU cache, so we don't need to check the LRU cache explicitly.
327 if (holder.bitmapRef != null) {
328 holder.bitmap = holder.bitmapRef.get();
329 if (holder.bitmap != null) {
330 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800331 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800332 }
333
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800334 try {
335 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
336 holder.bitmap = bitmap;
337 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
338 } catch (OutOfMemoryError e) {
339 // Do nothing - the photo will appear to be missing
340 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800341 }
342
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700343 public void clear() {
344 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800345 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700346 }
347
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800348 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800349 public void pause() {
350 mPaused = true;
351 }
352
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800353 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800354 public void resume() {
355 mPaused = false;
356 if (!mPendingRequests.isEmpty()) {
357 requestLoading();
358 }
359 }
360
361 /**
362 * Sends a message to this thread itself to start loading images. If the current
363 * view contains multiple image views, all of those image views will get a chance
364 * to request their respective photos before any of those requests are executed.
365 * This allows us to load images in bulk.
366 */
367 private void requestLoading() {
368 if (!mLoadingRequested) {
369 mLoadingRequested = true;
370 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
371 }
372 }
373
374 /**
375 * Processes requests on the main thread.
376 */
377 public boolean handleMessage(Message msg) {
378 switch (msg.what) {
379 case MESSAGE_REQUEST_LOADING: {
380 mLoadingRequested = false;
381 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800382 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800383 mLoaderThread.requestLoading();
384 }
385 return true;
386 }
387
388 case MESSAGE_PHOTOS_LOADED: {
389 if (!mPaused) {
390 processLoadedImages();
391 }
392 return true;
393 }
394 }
395 return false;
396 }
397
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800398 public void ensureLoaderThread() {
399 if (mLoaderThread == null) {
400 mLoaderThread = new LoaderThread(mContext.getContentResolver());
401 mLoaderThread.start();
402 }
403 }
404
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800405 /**
406 * Goes over pending loading requests and displays loaded photos. If some of the
407 * photos still haven't been loaded, sends another request for image loading.
408 */
409 private void processLoadedImages() {
410 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
411 while (iterator.hasNext()) {
412 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700413 Object key = mPendingRequests.get(view);
414 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800415 if (loaded) {
416 iterator.remove();
417 }
418 }
419
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800420 softenCache();
421
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800422 if (!mPendingRequests.isEmpty()) {
423 requestLoading();
424 }
425 }
426
427 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800428 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800429 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800430 */
431 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800432 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800433 holder.bitmap = null;
434 }
435 }
436
437 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800438 * Stores the supplied bitmap in cache.
439 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800440 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
441 BitmapHolder holder = new BitmapHolder(bytes);
442 holder.fresh = true;
443
444 // Unless this image is being preloaded, decode it right away while
445 // we are still on the background thread.
446 if (!preloading) {
447 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800448 }
449
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800450 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800451 }
452
453 /**
454 * Populates an array of photo IDs that need to be loaded.
455 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100456 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
457 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800458 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800459 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700460 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800461
462 /*
463 * Since the call is made from the loader thread, the map could be
464 * changing during the iteration. That's not really a problem:
465 * ConcurrentHashMap will allow those changes to happen without throwing
466 * exceptions. Since we may miss some requests in the situation of
467 * concurrent change, we will need to check the map again once loading
468 * is complete.
469 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700470 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800471 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700472 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800473 BitmapHolder holder = mBitmapHolderCache.get(key);
474 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700475 if (key instanceof Long) {
476 photoIds.add((Long)key);
477 photoIdsAsStrings.add(key.toString());
478 } else {
479 uris.add((Uri)key);
480 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800481 }
482 }
483 }
484
485 /**
486 * The thread that performs loading of photos from the database.
487 */
488 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700489 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800490 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
491 private static final int MESSAGE_LOAD_PHOTOS = 1;
492
493 /**
494 * A pause between preload batches that yields to the UI thread.
495 */
496 private static final int PHOTO_PRELOAD_DELAY = 50;
497
498 /**
499 * Number of photos to preload per batch.
500 */
501 private static final int PRELOAD_BATCH = 25;
502
503 /**
504 * Maximum number of photos to preload. If the cache size is 2Mb and
505 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
506 */
507 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700508
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800509 private final ContentResolver mResolver;
510 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100511 private final Set<Long> mPhotoIds = Sets.newHashSet();
512 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
513 private final Set<Uri> mPhotoUris = Sets.newHashSet();
514 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800515
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800516 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700517 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800518
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800519 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
520 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
521 private static final int PRELOAD_STATUS_DONE = 2;
522
523 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
524
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800525 public LoaderThread(ContentResolver resolver) {
526 super(LOADER_THREAD_NAME);
527 mResolver = resolver;
528 }
529
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800530 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800531 if (mLoaderThreadHandler == null) {
532 mLoaderThreadHandler = new Handler(getLooper(), this);
533 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800534 }
535
536 /**
537 * Kicks off preloading of the next batch of photos on the background thread.
538 * Preloading will happen after a delay: we want to yield to the UI thread
539 * as much as possible.
540 * <p>
541 * If preloading is already complete, does nothing.
542 */
543 public void requestPreloading() {
544 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
545 return;
546 }
547
548 ensureHandler();
549 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
550 return;
551 }
552
553 mLoaderThreadHandler.sendEmptyMessageDelayed(
554 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
555 }
556
557 /**
558 * Sends a message to this thread to load requested photos. Cancels a preloading
559 * request, if any: we don't want preloading to impede loading of the photos
560 * we need to display now.
561 */
562 public void requestLoading() {
563 ensureHandler();
564 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
565 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800566 }
567
568 /**
569 * Receives the above message, loads photos and then sends a message
570 * to the main thread to process them.
571 */
572 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800573 switch (msg.what) {
574 case MESSAGE_PRELOAD_PHOTOS:
575 preloadPhotosInBackground();
576 break;
577 case MESSAGE_LOAD_PHOTOS:
578 loadPhotosInBackground();
579 break;
580 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800581 return true;
582 }
583
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800584 /**
585 * The first time it is called, figures out which photos need to be preloaded.
586 * Each subsequent call preloads the next batch of photos and requests
587 * another cycle of preloading after a delay. The whole process ends when
588 * we either run out of photos to preload or fill up cache.
589 */
590 private void preloadPhotosInBackground() {
591 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
592 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800593 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800594
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800595 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
596 queryPhotosForPreload();
597 if (mPreloadPhotoIds.isEmpty()) {
598 mPreloadStatus = PRELOAD_STATUS_DONE;
599 } else {
600 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
601 }
602 requestPreloading();
603 return;
604 }
605
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800606 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800607 mPreloadStatus = PRELOAD_STATUS_DONE;
608 return;
609 }
610
611 mPhotoIds.clear();
612 mPhotoIdsAsStrings.clear();
613
614 int count = 0;
615 int preloadSize = mPreloadPhotoIds.size();
616 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
617 preloadSize--;
618 count++;
619 Long photoId = mPreloadPhotoIds.get(preloadSize);
620 mPhotoIds.add(photoId);
621 mPhotoIdsAsStrings.add(photoId.toString());
622 mPreloadPhotoIds.remove(preloadSize);
623 }
624
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700625 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800626
627 if (preloadSize == 0) {
628 mPreloadStatus = PRELOAD_STATUS_DONE;
629 }
630
631 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
632 + mBitmapHolderCache.size()
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800633 + ". Total size: " + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800634
635 requestPreloading();
636 }
637
638 private void queryPhotosForPreload() {
639 Cursor cursor = null;
640 try {
641 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
642 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700643 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
644 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dave Santoro11a9df72011-06-30 11:33:28 -0700645 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1")
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800646 .build();
647 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
648 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
649 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700650 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800651
652 if (cursor != null) {
653 while (cursor.moveToNext()) {
654 // Insert them in reverse order, because we will be taking
655 // them from the end of the list for loading.
656 mPreloadPhotoIds.add(0, cursor.getLong(0));
657 }
658 }
659 } finally {
660 if (cursor != null) {
661 cursor.close();
662 }
663 }
664 }
665
666 private void loadPhotosInBackground() {
667 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700668 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800669 loadRemotePhotos();
670 requestPreloading();
671 }
672
673 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100674 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800675 return;
676 }
677
678 // Remove loaded photos from the preload queue: we don't want
679 // the preloading process to load them again.
680 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100681 for (Long id : mPhotoIds) {
682 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800683 }
684 if (mPreloadPhotoIds.isEmpty()) {
685 mPreloadStatus = PRELOAD_STATUS_DONE;
686 }
687 }
688
689 mStringBuilder.setLength(0);
690 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100691 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800692 if (i != 0) {
693 mStringBuilder.append(',');
694 }
695 mStringBuilder.append('?');
696 }
697 mStringBuilder.append(')');
698
699 Cursor cursor = null;
700 try {
Dave Santoro11a9df72011-06-30 11:33:28 -0700701 cursor = mResolver.query(Data.CONTENT_URI.buildUpon()
702 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1").build(),
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800703 COLUMNS,
704 mStringBuilder.toString(),
705 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
706 null);
707
708 if (cursor != null) {
709 while (cursor.moveToNext()) {
710 Long id = cursor.getLong(0);
711 byte[] bytes = cursor.getBlob(1);
712 cacheBitmap(id, bytes, preloading);
713 mPhotoIds.remove(id);
714 }
715 }
716 } finally {
717 if (cursor != null) {
718 cursor.close();
719 }
720 }
721
722 // Remaining photos were not found in the database - mark the cache accordingly.
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100723 for (Long id : mPhotoIds) {
724 cacheBitmap(id, null, preloading);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800725 }
726
727 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
728 }
729
730 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100731 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700732 if (mBuffer == null) {
733 mBuffer = new byte[BUFFER_SIZE];
734 }
735 try {
736 InputStream is = mResolver.openInputStream(uri);
737 if (is != null) {
738 ByteArrayOutputStream baos = new ByteArrayOutputStream();
739 try {
740 int size;
741 while ((size = is.read(mBuffer)) != -1) {
742 baos.write(mBuffer, 0, size);
743 }
744 } finally {
745 is.close();
746 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800747 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700748 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700749 } else {
750 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800751 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700752 }
753 } catch (Exception ex) {
754 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800755 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700756 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800757 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800758 }
759 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800760}