blob: 150856a9ed46d07929768217c29d41392d288b29 [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;
21
22import android.content.ContentResolver;
23import android.content.Context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080024import android.content.res.Resources;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080025import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070028import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080029import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070030import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080031import android.os.HandlerThread;
32import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080033import android.provider.ContactsContract;
34import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080035import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070036import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080037import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070038import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080039import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080040import android.widget.ImageView;
41
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070042import java.io.ByteArrayOutputStream;
43import java.io.InputStream;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080044import java.lang.ref.SoftReference;
45import java.util.ArrayList;
46import java.util.Iterator;
47import java.util.concurrent.ConcurrentHashMap;
48
49/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080050 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080051 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080052public abstract class ContactPhotoManager {
53
54 static final String TAG = "ContactPhotoManager";
55
56 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
57
58 /**
59 * The resource ID of the image to be used when the photo is unavailable or being
60 * loaded.
61 */
62 protected final int mDefaultResourceId = R.drawable.ic_contact_picture;
63
64 /**
65 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
66 * the available authenticators. This method can safely be called from the UI thread.
67 */
68 public static ContactPhotoManager getInstance(Context context) {
69 ContactPhotoManager service =
70 (ContactPhotoManager) context.getSystemService(CONTACT_PHOTO_SERVICE);
71 if (service == null) {
72 service = createContactPhotoManager(context);
73 Log.e(TAG, "No contact photo service in context: " + context);
74 }
75 return service;
76 }
77
78 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
79 return new ContactPhotoManagerImpl(context);
80 }
81
82 /**
83 * Load photo into the supplied image view. If the photo is already cached,
84 * it is displayed immediately. Otherwise a request is sent to load the photo
85 * from the database.
86 */
87 public abstract void loadPhoto(ImageView view, long photoId);
88
89 /**
90 * Load photo into the supplied image view. If the photo is already cached,
91 * it is displayed immediately. Otherwise a request is sent to load the photo
92 * from the location specified by the URI.
93 */
94 public abstract void loadPhoto(ImageView view, Uri photoUri);
95
96 /**
97 * Temporarily stops loading photos from the database.
98 */
99 public abstract void pause();
100
101 /**
102 * Resumes loading photos from the database.
103 */
104 public abstract void resume();
105
106 /**
107 * Marks all cached photos for reloading. We can continue using cache but should
108 * also make sure the photos haven't changed in the background and notify the views
109 * if so.
110 */
111 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800112
113 /**
114 * Initiates a background process that over time will fill up cache with
115 * preload photos.
116 */
117 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800118}
119
120class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800121 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
122
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800123 /**
124 * Type of message sent by the UI thread to itself to indicate that some photos
125 * need to be loaded.
126 */
127 private static final int MESSAGE_REQUEST_LOADING = 1;
128
129 /**
130 * Type of message sent by the loader thread to indicate that some photos have
131 * been loaded.
132 */
133 private static final int MESSAGE_PHOTOS_LOADED = 2;
134
135 private static final String[] EMPTY_STRING_ARRAY = new String[0];
136
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800137 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800138
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800139 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800140 * Maintains the state of a particular photo.
141 */
142 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800143 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800144
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800145 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800146 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800147 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800148
149 public BitmapHolder(byte[] bytes) {
150 this.bytes = bytes;
151 this.fresh = true;
152 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800153 }
154
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800155 private final Context mContext;
156
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800157 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800158 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800159 * as they come from the database. Each holder has a soft reference to the
160 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800161 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800162 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
163
164 /**
165 * Cache size threshold at which bitmaps will not be preloaded.
166 */
167 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800168
169 /**
170 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
171 * the most recently used bitmaps to save time on decoding
172 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
173 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800174 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800175
176 /**
177 * A map from ImageView to the corresponding photo ID. Please note that this
178 * photo ID may change before the photo loading request is started.
179 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700180 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
181 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800182
183 /**
184 * Handler for messages sent to the UI thread.
185 */
186 private final Handler mMainThreadHandler = new Handler(this);
187
188 /**
189 * Thread responsible for loading photos from the database. Created upon
190 * the first request.
191 */
192 private LoaderThread mLoaderThread;
193
194 /**
195 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
196 */
197 private boolean mLoadingRequested;
198
199 /**
200 * Flag indicating if the image loading is paused.
201 */
202 private boolean mPaused;
203
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800204 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800205 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800206
207 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800208 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800209 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800210 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
211 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
212 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800213 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800214 }
215 };
216 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800217 }
218
219 @Override
220 public void preloadPhotosInBackground() {
221 ensureLoaderThread();
222 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800223 }
224
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800225 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800226 public void loadPhoto(ImageView view, long photoId) {
227 if (photoId == 0) {
228 // No photo is needed
229 view.setImageResource(mDefaultResourceId);
230 mPendingRequests.remove(view);
231 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700232 loadPhotoByIdOrUri(view, photoId);
233 }
234 }
235
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800236 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700237 public void loadPhoto(ImageView view, Uri photoUri) {
238 if (photoUri == null) {
239 // No photo is needed
240 view.setImageResource(mDefaultResourceId);
241 mPendingRequests.remove(view);
242 } else {
243 loadPhotoByIdOrUri(view, photoUri);
244 }
245 }
246
247 private void loadPhotoByIdOrUri(ImageView view, Object key) {
248 boolean loaded = loadCachedPhoto(view, key);
249 if (loaded) {
250 mPendingRequests.remove(view);
251 } else {
252 mPendingRequests.put(view, key);
253 if (!mPaused) {
254 // Send a request to start loading photos
255 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800256 }
257 }
258 }
259
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800260 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800261 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800262 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800263 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800264 }
265 }
266
267 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800268 * Checks if the photo is present in cache. If so, sets the photo on the view.
269 *
270 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800271 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700272 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800273 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800274 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800275 // The bitmap has not been loaded - should display the placeholder image.
276 view.setImageResource(mDefaultResourceId);
277 return false;
278 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800279
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800280 if (holder.bytes == null) {
281 view.setImageResource(mDefaultResourceId);
282 return holder.fresh;
283 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800284
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800285 // Optionally decode bytes into a bitmap
286 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800287
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800288 view.setImageBitmap(holder.bitmap);
289
290 // Put the bitmap in the LRU cache
291 mBitmapCache.put(key, holder.bitmap);
292
293 // Soften the reference
294 holder.bitmap = null;
295
296 return holder.fresh;
297 }
298
299 /**
300 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
301 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
302 * the holder, it will not be necessary to decode the bitmap.
303 */
304 private void inflateBitmap(BitmapHolder holder) {
305 byte[] bytes = holder.bytes;
306 if (bytes == null || bytes.length == 0) {
307 return;
308 }
309
310 // Check the soft reference. If will be retained if the bitmap is also
311 // in the LRU cache, so we don't need to check the LRU cache explicitly.
312 if (holder.bitmapRef != null) {
313 holder.bitmap = holder.bitmapRef.get();
314 if (holder.bitmap != null) {
315 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800316 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800317 }
318
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800319 try {
320 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
321 holder.bitmap = bitmap;
322 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
323 } catch (OutOfMemoryError e) {
324 // Do nothing - the photo will appear to be missing
325 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800326 }
327
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700328 public void clear() {
329 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800330 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700331 }
332
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800333 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800334 public void pause() {
335 mPaused = true;
336 }
337
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800338 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800339 public void resume() {
340 mPaused = false;
341 if (!mPendingRequests.isEmpty()) {
342 requestLoading();
343 }
344 }
345
346 /**
347 * Sends a message to this thread itself to start loading images. If the current
348 * view contains multiple image views, all of those image views will get a chance
349 * to request their respective photos before any of those requests are executed.
350 * This allows us to load images in bulk.
351 */
352 private void requestLoading() {
353 if (!mLoadingRequested) {
354 mLoadingRequested = true;
355 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
356 }
357 }
358
359 /**
360 * Processes requests on the main thread.
361 */
362 public boolean handleMessage(Message msg) {
363 switch (msg.what) {
364 case MESSAGE_REQUEST_LOADING: {
365 mLoadingRequested = false;
366 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800367 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800368 mLoaderThread.requestLoading();
369 }
370 return true;
371 }
372
373 case MESSAGE_PHOTOS_LOADED: {
374 if (!mPaused) {
375 processLoadedImages();
376 }
377 return true;
378 }
379 }
380 return false;
381 }
382
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800383 public void ensureLoaderThread() {
384 if (mLoaderThread == null) {
385 mLoaderThread = new LoaderThread(mContext.getContentResolver());
386 mLoaderThread.start();
387 }
388 }
389
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800390 /**
391 * Goes over pending loading requests and displays loaded photos. If some of the
392 * photos still haven't been loaded, sends another request for image loading.
393 */
394 private void processLoadedImages() {
395 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
396 while (iterator.hasNext()) {
397 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700398 Object key = mPendingRequests.get(view);
399 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800400 if (loaded) {
401 iterator.remove();
402 }
403 }
404
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800405 softenCache();
406
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800407 if (!mPendingRequests.isEmpty()) {
408 requestLoading();
409 }
410 }
411
412 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800413 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800414 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800415 */
416 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800417 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800418 holder.bitmap = null;
419 }
420 }
421
422 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800423 * Stores the supplied bitmap in cache.
424 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800425 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
426 BitmapHolder holder = new BitmapHolder(bytes);
427 holder.fresh = true;
428
429 // Unless this image is being preloaded, decode it right away while
430 // we are still on the background thread.
431 if (!preloading) {
432 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800433 }
434
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800435 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800436 }
437
438 /**
439 * Populates an array of photo IDs that need to be loaded.
440 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700441 private void obtainPhotoIdsAndUrisToLoad(ArrayList<Long> photoIds,
442 ArrayList<String> photoIdsAsStrings, ArrayList<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800443 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800444 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700445 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800446
447 /*
448 * Since the call is made from the loader thread, the map could be
449 * changing during the iteration. That's not really a problem:
450 * ConcurrentHashMap will allow those changes to happen without throwing
451 * exceptions. Since we may miss some requests in the situation of
452 * concurrent change, we will need to check the map again once loading
453 * is complete.
454 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700455 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800456 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700457 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800458 BitmapHolder holder = mBitmapHolderCache.get(key);
459 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700460 if (key instanceof Long) {
461 photoIds.add((Long)key);
462 photoIdsAsStrings.add(key.toString());
463 } else {
464 uris.add((Uri)key);
465 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800466 }
467 }
468 }
469
470 /**
471 * The thread that performs loading of photos from the database.
472 */
473 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700474 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800475 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
476 private static final int MESSAGE_LOAD_PHOTOS = 1;
477
478 /**
479 * A pause between preload batches that yields to the UI thread.
480 */
481 private static final int PHOTO_PRELOAD_DELAY = 50;
482
483 /**
484 * Number of photos to preload per batch.
485 */
486 private static final int PRELOAD_BATCH = 25;
487
488 /**
489 * Maximum number of photos to preload. If the cache size is 2Mb and
490 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
491 */
492 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700493
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800494 private final ContentResolver mResolver;
495 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800496 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
497 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700498 private final ArrayList<Uri> mPhotoUris = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800499 private ArrayList<Long> mPreloadPhotoIds = Lists.newArrayList();
500
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800501 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700502 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800503
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800504 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
505 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
506 private static final int PRELOAD_STATUS_DONE = 2;
507
508 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
509
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800510 public LoaderThread(ContentResolver resolver) {
511 super(LOADER_THREAD_NAME);
512 mResolver = resolver;
513 }
514
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800515 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800516 if (mLoaderThreadHandler == null) {
517 mLoaderThreadHandler = new Handler(getLooper(), this);
518 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800519 }
520
521 /**
522 * Kicks off preloading of the next batch of photos on the background thread.
523 * Preloading will happen after a delay: we want to yield to the UI thread
524 * as much as possible.
525 * <p>
526 * If preloading is already complete, does nothing.
527 */
528 public void requestPreloading() {
529 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
530 return;
531 }
532
533 ensureHandler();
534 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
535 return;
536 }
537
538 mLoaderThreadHandler.sendEmptyMessageDelayed(
539 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
540 }
541
542 /**
543 * Sends a message to this thread to load requested photos. Cancels a preloading
544 * request, if any: we don't want preloading to impede loading of the photos
545 * we need to display now.
546 */
547 public void requestLoading() {
548 ensureHandler();
549 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
550 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800551 }
552
553 /**
554 * Receives the above message, loads photos and then sends a message
555 * to the main thread to process them.
556 */
557 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800558 switch (msg.what) {
559 case MESSAGE_PRELOAD_PHOTOS:
560 preloadPhotosInBackground();
561 break;
562 case MESSAGE_LOAD_PHOTOS:
563 loadPhotosInBackground();
564 break;
565 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800566 return true;
567 }
568
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800569 /**
570 * The first time it is called, figures out which photos need to be preloaded.
571 * Each subsequent call preloads the next batch of photos and requests
572 * another cycle of preloading after a delay. The whole process ends when
573 * we either run out of photos to preload or fill up cache.
574 */
575 private void preloadPhotosInBackground() {
576 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
577 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800578 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800579
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800580 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
581 queryPhotosForPreload();
582 if (mPreloadPhotoIds.isEmpty()) {
583 mPreloadStatus = PRELOAD_STATUS_DONE;
584 } else {
585 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
586 }
587 requestPreloading();
588 return;
589 }
590
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800591 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800592 mPreloadStatus = PRELOAD_STATUS_DONE;
593 return;
594 }
595
596 mPhotoIds.clear();
597 mPhotoIdsAsStrings.clear();
598
599 int count = 0;
600 int preloadSize = mPreloadPhotoIds.size();
601 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
602 preloadSize--;
603 count++;
604 Long photoId = mPreloadPhotoIds.get(preloadSize);
605 mPhotoIds.add(photoId);
606 mPhotoIdsAsStrings.add(photoId.toString());
607 mPreloadPhotoIds.remove(preloadSize);
608 }
609
610 loadPhotosFromDatabase(false);
611
612 if (preloadSize == 0) {
613 mPreloadStatus = PRELOAD_STATUS_DONE;
614 }
615
616 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
617 + mBitmapHolderCache.size()
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800618 + ". Total size: " + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800619
620 requestPreloading();
621 }
622
623 private void queryPhotosForPreload() {
624 Cursor cursor = null;
625 try {
626 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
627 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700628 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
629 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800630 .build();
631 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
632 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
633 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700634 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800635
636 if (cursor != null) {
637 while (cursor.moveToNext()) {
638 // Insert them in reverse order, because we will be taking
639 // them from the end of the list for loading.
640 mPreloadPhotoIds.add(0, cursor.getLong(0));
641 }
642 }
643 } finally {
644 if (cursor != null) {
645 cursor.close();
646 }
647 }
648 }
649
650 private void loadPhotosInBackground() {
651 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
652 loadPhotosFromDatabase(true);
653 loadRemotePhotos();
654 requestPreloading();
655 }
656
657 private void loadPhotosFromDatabase(boolean preloading) {
658 int count = mPhotoIds.size();
659 if (count == 0) {
660 return;
661 }
662
663 // Remove loaded photos from the preload queue: we don't want
664 // the preloading process to load them again.
665 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
666 for (int i = 0; i < count; i++) {
667 mPreloadPhotoIds.remove(mPhotoIds.get(i));
668 }
669 if (mPreloadPhotoIds.isEmpty()) {
670 mPreloadStatus = PRELOAD_STATUS_DONE;
671 }
672 }
673
674 mStringBuilder.setLength(0);
675 mStringBuilder.append(Photo._ID + " IN(");
676 for (int i = 0; i < count; i++) {
677 if (i != 0) {
678 mStringBuilder.append(',');
679 }
680 mStringBuilder.append('?');
681 }
682 mStringBuilder.append(')');
683
684 Cursor cursor = null;
685 try {
686 cursor = mResolver.query(Data.CONTENT_URI,
687 COLUMNS,
688 mStringBuilder.toString(),
689 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
690 null);
691
692 if (cursor != null) {
693 while (cursor.moveToNext()) {
694 Long id = cursor.getLong(0);
695 byte[] bytes = cursor.getBlob(1);
696 cacheBitmap(id, bytes, preloading);
697 mPhotoIds.remove(id);
698 }
699 }
700 } finally {
701 if (cursor != null) {
702 cursor.close();
703 }
704 }
705
706 // Remaining photos were not found in the database - mark the cache accordingly.
707 count = mPhotoIds.size();
708 for (int i = 0; i < count; i++) {
709 cacheBitmap(mPhotoIds.get(i), null, preloading);
710 }
711
712 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
713 }
714
715 private void loadRemotePhotos() {
716 int count = mPhotoUris.size();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800717 for (int i = 0; i < count; i++) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700718 Uri uri = mPhotoUris.get(i);
719 if (mBuffer == null) {
720 mBuffer = new byte[BUFFER_SIZE];
721 }
722 try {
723 InputStream is = mResolver.openInputStream(uri);
724 if (is != null) {
725 ByteArrayOutputStream baos = new ByteArrayOutputStream();
726 try {
727 int size;
728 while ((size = is.read(mBuffer)) != -1) {
729 baos.write(mBuffer, 0, size);
730 }
731 } finally {
732 is.close();
733 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800734 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700735 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700736 } else {
737 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800738 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700739 }
740 } catch (Exception ex) {
741 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800742 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700743 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800744 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800745 }
746 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800747}