blob: fe73fbe45c81409b5511e4a7b25515acffa52be9 [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) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010069 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080070 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +010071 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080072 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010073 service = createContactPhotoManager(applicationContext);
74 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080075 }
76 return service;
77 }
78
79 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
80 return new ContactPhotoManagerImpl(context);
81 }
82
83 /**
84 * Load photo into the supplied image view. If the photo is already cached,
85 * it is displayed immediately. Otherwise a request is sent to load the photo
86 * from the database.
87 */
88 public abstract void loadPhoto(ImageView view, long photoId);
89
90 /**
91 * Load photo into the supplied image view. If the photo is already cached,
92 * it is displayed immediately. Otherwise a request is sent to load the photo
93 * from the location specified by the URI.
94 */
95 public abstract void loadPhoto(ImageView view, Uri photoUri);
96
97 /**
98 * Temporarily stops loading photos from the database.
99 */
100 public abstract void pause();
101
102 /**
103 * Resumes loading photos from the database.
104 */
105 public abstract void resume();
106
107 /**
108 * Marks all cached photos for reloading. We can continue using cache but should
109 * also make sure the photos haven't changed in the background and notify the views
110 * if so.
111 */
112 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800113
114 /**
115 * Initiates a background process that over time will fill up cache with
116 * preload photos.
117 */
118 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800119}
120
121class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800122 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
123
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800124 /**
125 * Type of message sent by the UI thread to itself to indicate that some photos
126 * need to be loaded.
127 */
128 private static final int MESSAGE_REQUEST_LOADING = 1;
129
130 /**
131 * Type of message sent by the loader thread to indicate that some photos have
132 * been loaded.
133 */
134 private static final int MESSAGE_PHOTOS_LOADED = 2;
135
136 private static final String[] EMPTY_STRING_ARRAY = new String[0];
137
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800138 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800139
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800140 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800141 * Maintains the state of a particular photo.
142 */
143 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800144 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800145
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800146 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800147 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800148 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800149
150 public BitmapHolder(byte[] bytes) {
151 this.bytes = bytes;
152 this.fresh = true;
153 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800154 }
155
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800156 private final Context mContext;
157
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800158 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800159 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800160 * as they come from the database. Each holder has a soft reference to the
161 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800162 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800163 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
164
165 /**
166 * Cache size threshold at which bitmaps will not be preloaded.
167 */
168 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800169
170 /**
171 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
172 * the most recently used bitmaps to save time on decoding
173 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
174 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800175 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800176
177 /**
178 * A map from ImageView to the corresponding photo ID. Please note that this
179 * photo ID may change before the photo loading request is started.
180 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700181 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
182 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800183
184 /**
185 * Handler for messages sent to the UI thread.
186 */
187 private final Handler mMainThreadHandler = new Handler(this);
188
189 /**
190 * Thread responsible for loading photos from the database. Created upon
191 * the first request.
192 */
193 private LoaderThread mLoaderThread;
194
195 /**
196 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
197 */
198 private boolean mLoadingRequested;
199
200 /**
201 * Flag indicating if the image loading is paused.
202 */
203 private boolean mPaused;
204
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800205 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800206 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800207
208 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800209 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800210 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800211 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
212 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
213 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800214 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800215 }
216 };
217 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800218 }
219
220 @Override
221 public void preloadPhotosInBackground() {
222 ensureLoaderThread();
223 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800224 }
225
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800226 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800227 public void loadPhoto(ImageView view, long photoId) {
228 if (photoId == 0) {
229 // No photo is needed
230 view.setImageResource(mDefaultResourceId);
231 mPendingRequests.remove(view);
232 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700233 loadPhotoByIdOrUri(view, photoId);
234 }
235 }
236
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800237 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700238 public void loadPhoto(ImageView view, Uri photoUri) {
239 if (photoUri == null) {
240 // No photo is needed
241 view.setImageResource(mDefaultResourceId);
242 mPendingRequests.remove(view);
243 } else {
244 loadPhotoByIdOrUri(view, photoUri);
245 }
246 }
247
248 private void loadPhotoByIdOrUri(ImageView view, Object key) {
249 boolean loaded = loadCachedPhoto(view, key);
250 if (loaded) {
251 mPendingRequests.remove(view);
252 } else {
253 mPendingRequests.put(view, key);
254 if (!mPaused) {
255 // Send a request to start loading photos
256 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800257 }
258 }
259 }
260
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800261 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800262 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800263 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800264 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800265 }
266 }
267
268 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800269 * Checks if the photo is present in cache. If so, sets the photo on the view.
270 *
271 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800272 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700273 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800274 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800275 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800276 // The bitmap has not been loaded - should display the placeholder image.
277 view.setImageResource(mDefaultResourceId);
278 return false;
279 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800280
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800281 if (holder.bytes == null) {
282 view.setImageResource(mDefaultResourceId);
283 return holder.fresh;
284 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800285
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800286 // Optionally decode bytes into a bitmap
287 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800288
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800289 view.setImageBitmap(holder.bitmap);
290
291 // Put the bitmap in the LRU cache
292 mBitmapCache.put(key, holder.bitmap);
293
294 // Soften the reference
295 holder.bitmap = null;
296
297 return holder.fresh;
298 }
299
300 /**
301 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
302 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
303 * the holder, it will not be necessary to decode the bitmap.
304 */
305 private void inflateBitmap(BitmapHolder holder) {
306 byte[] bytes = holder.bytes;
307 if (bytes == null || bytes.length == 0) {
308 return;
309 }
310
311 // Check the soft reference. If will be retained if the bitmap is also
312 // in the LRU cache, so we don't need to check the LRU cache explicitly.
313 if (holder.bitmapRef != null) {
314 holder.bitmap = holder.bitmapRef.get();
315 if (holder.bitmap != null) {
316 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800317 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800318 }
319
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800320 try {
321 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
322 holder.bitmap = bitmap;
323 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
324 } catch (OutOfMemoryError e) {
325 // Do nothing - the photo will appear to be missing
326 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800327 }
328
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700329 public void clear() {
330 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800331 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700332 }
333
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800334 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800335 public void pause() {
336 mPaused = true;
337 }
338
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800339 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800340 public void resume() {
341 mPaused = false;
342 if (!mPendingRequests.isEmpty()) {
343 requestLoading();
344 }
345 }
346
347 /**
348 * Sends a message to this thread itself to start loading images. If the current
349 * view contains multiple image views, all of those image views will get a chance
350 * to request their respective photos before any of those requests are executed.
351 * This allows us to load images in bulk.
352 */
353 private void requestLoading() {
354 if (!mLoadingRequested) {
355 mLoadingRequested = true;
356 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
357 }
358 }
359
360 /**
361 * Processes requests on the main thread.
362 */
363 public boolean handleMessage(Message msg) {
364 switch (msg.what) {
365 case MESSAGE_REQUEST_LOADING: {
366 mLoadingRequested = false;
367 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800368 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800369 mLoaderThread.requestLoading();
370 }
371 return true;
372 }
373
374 case MESSAGE_PHOTOS_LOADED: {
375 if (!mPaused) {
376 processLoadedImages();
377 }
378 return true;
379 }
380 }
381 return false;
382 }
383
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800384 public void ensureLoaderThread() {
385 if (mLoaderThread == null) {
386 mLoaderThread = new LoaderThread(mContext.getContentResolver());
387 mLoaderThread.start();
388 }
389 }
390
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800391 /**
392 * Goes over pending loading requests and displays loaded photos. If some of the
393 * photos still haven't been loaded, sends another request for image loading.
394 */
395 private void processLoadedImages() {
396 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
397 while (iterator.hasNext()) {
398 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700399 Object key = mPendingRequests.get(view);
400 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800401 if (loaded) {
402 iterator.remove();
403 }
404 }
405
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800406 softenCache();
407
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800408 if (!mPendingRequests.isEmpty()) {
409 requestLoading();
410 }
411 }
412
413 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800414 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800415 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800416 */
417 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800418 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800419 holder.bitmap = null;
420 }
421 }
422
423 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800424 * Stores the supplied bitmap in cache.
425 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800426 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
427 BitmapHolder holder = new BitmapHolder(bytes);
428 holder.fresh = true;
429
430 // Unless this image is being preloaded, decode it right away while
431 // we are still on the background thread.
432 if (!preloading) {
433 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800434 }
435
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800436 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800437 }
438
439 /**
440 * Populates an array of photo IDs that need to be loaded.
441 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700442 private void obtainPhotoIdsAndUrisToLoad(ArrayList<Long> photoIds,
443 ArrayList<String> photoIdsAsStrings, ArrayList<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800444 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800445 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700446 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800447
448 /*
449 * Since the call is made from the loader thread, the map could be
450 * changing during the iteration. That's not really a problem:
451 * ConcurrentHashMap will allow those changes to happen without throwing
452 * exceptions. Since we may miss some requests in the situation of
453 * concurrent change, we will need to check the map again once loading
454 * is complete.
455 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700456 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800457 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700458 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800459 BitmapHolder holder = mBitmapHolderCache.get(key);
460 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700461 if (key instanceof Long) {
462 photoIds.add((Long)key);
463 photoIdsAsStrings.add(key.toString());
464 } else {
465 uris.add((Uri)key);
466 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800467 }
468 }
469 }
470
471 /**
472 * The thread that performs loading of photos from the database.
473 */
474 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700475 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800476 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
477 private static final int MESSAGE_LOAD_PHOTOS = 1;
478
479 /**
480 * A pause between preload batches that yields to the UI thread.
481 */
482 private static final int PHOTO_PRELOAD_DELAY = 50;
483
484 /**
485 * Number of photos to preload per batch.
486 */
487 private static final int PRELOAD_BATCH = 25;
488
489 /**
490 * Maximum number of photos to preload. If the cache size is 2Mb and
491 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
492 */
493 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700494
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800495 private final ContentResolver mResolver;
496 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800497 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
498 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700499 private final ArrayList<Uri> mPhotoUris = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800500 private ArrayList<Long> mPreloadPhotoIds = Lists.newArrayList();
501
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800502 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700503 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800504
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800505 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
506 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
507 private static final int PRELOAD_STATUS_DONE = 2;
508
509 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
510
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800511 public LoaderThread(ContentResolver resolver) {
512 super(LOADER_THREAD_NAME);
513 mResolver = resolver;
514 }
515
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800516 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800517 if (mLoaderThreadHandler == null) {
518 mLoaderThreadHandler = new Handler(getLooper(), this);
519 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800520 }
521
522 /**
523 * Kicks off preloading of the next batch of photos on the background thread.
524 * Preloading will happen after a delay: we want to yield to the UI thread
525 * as much as possible.
526 * <p>
527 * If preloading is already complete, does nothing.
528 */
529 public void requestPreloading() {
530 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
531 return;
532 }
533
534 ensureHandler();
535 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
536 return;
537 }
538
539 mLoaderThreadHandler.sendEmptyMessageDelayed(
540 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
541 }
542
543 /**
544 * Sends a message to this thread to load requested photos. Cancels a preloading
545 * request, if any: we don't want preloading to impede loading of the photos
546 * we need to display now.
547 */
548 public void requestLoading() {
549 ensureHandler();
550 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
551 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800552 }
553
554 /**
555 * Receives the above message, loads photos and then sends a message
556 * to the main thread to process them.
557 */
558 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800559 switch (msg.what) {
560 case MESSAGE_PRELOAD_PHOTOS:
561 preloadPhotosInBackground();
562 break;
563 case MESSAGE_LOAD_PHOTOS:
564 loadPhotosInBackground();
565 break;
566 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800567 return true;
568 }
569
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800570 /**
571 * The first time it is called, figures out which photos need to be preloaded.
572 * Each subsequent call preloads the next batch of photos and requests
573 * another cycle of preloading after a delay. The whole process ends when
574 * we either run out of photos to preload or fill up cache.
575 */
576 private void preloadPhotosInBackground() {
577 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
578 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800579 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800580
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800581 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
582 queryPhotosForPreload();
583 if (mPreloadPhotoIds.isEmpty()) {
584 mPreloadStatus = PRELOAD_STATUS_DONE;
585 } else {
586 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
587 }
588 requestPreloading();
589 return;
590 }
591
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800592 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800593 mPreloadStatus = PRELOAD_STATUS_DONE;
594 return;
595 }
596
597 mPhotoIds.clear();
598 mPhotoIdsAsStrings.clear();
599
600 int count = 0;
601 int preloadSize = mPreloadPhotoIds.size();
602 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
603 preloadSize--;
604 count++;
605 Long photoId = mPreloadPhotoIds.get(preloadSize);
606 mPhotoIds.add(photoId);
607 mPhotoIdsAsStrings.add(photoId.toString());
608 mPreloadPhotoIds.remove(preloadSize);
609 }
610
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700611 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800612
613 if (preloadSize == 0) {
614 mPreloadStatus = PRELOAD_STATUS_DONE;
615 }
616
617 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
618 + mBitmapHolderCache.size()
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800619 + ". Total size: " + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800620
621 requestPreloading();
622 }
623
624 private void queryPhotosForPreload() {
625 Cursor cursor = null;
626 try {
627 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
628 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700629 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
630 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dave Santoro11a9df72011-06-30 11:33:28 -0700631 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1")
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800632 .build();
633 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
634 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
635 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700636 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800637
638 if (cursor != null) {
639 while (cursor.moveToNext()) {
640 // Insert them in reverse order, because we will be taking
641 // them from the end of the list for loading.
642 mPreloadPhotoIds.add(0, cursor.getLong(0));
643 }
644 }
645 } finally {
646 if (cursor != null) {
647 cursor.close();
648 }
649 }
650 }
651
652 private void loadPhotosInBackground() {
653 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700654 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800655 loadRemotePhotos();
656 requestPreloading();
657 }
658
659 private void loadPhotosFromDatabase(boolean preloading) {
660 int count = mPhotoIds.size();
661 if (count == 0) {
662 return;
663 }
664
665 // Remove loaded photos from the preload queue: we don't want
666 // the preloading process to load them again.
667 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
668 for (int i = 0; i < count; i++) {
669 mPreloadPhotoIds.remove(mPhotoIds.get(i));
670 }
671 if (mPreloadPhotoIds.isEmpty()) {
672 mPreloadStatus = PRELOAD_STATUS_DONE;
673 }
674 }
675
676 mStringBuilder.setLength(0);
677 mStringBuilder.append(Photo._ID + " IN(");
678 for (int i = 0; i < count; i++) {
679 if (i != 0) {
680 mStringBuilder.append(',');
681 }
682 mStringBuilder.append('?');
683 }
684 mStringBuilder.append(')');
685
686 Cursor cursor = null;
687 try {
Dave Santoro11a9df72011-06-30 11:33:28 -0700688 cursor = mResolver.query(Data.CONTENT_URI.buildUpon()
689 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1").build(),
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800690 COLUMNS,
691 mStringBuilder.toString(),
692 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
693 null);
694
695 if (cursor != null) {
696 while (cursor.moveToNext()) {
697 Long id = cursor.getLong(0);
698 byte[] bytes = cursor.getBlob(1);
699 cacheBitmap(id, bytes, preloading);
700 mPhotoIds.remove(id);
701 }
702 }
703 } finally {
704 if (cursor != null) {
705 cursor.close();
706 }
707 }
708
709 // Remaining photos were not found in the database - mark the cache accordingly.
710 count = mPhotoIds.size();
711 for (int i = 0; i < count; i++) {
712 cacheBitmap(mPhotoIds.get(i), null, preloading);
713 }
714
715 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
716 }
717
718 private void loadRemotePhotos() {
719 int count = mPhotoUris.size();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800720 for (int i = 0; i < count; i++) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700721 Uri uri = mPhotoUris.get(i);
722 if (mBuffer == null) {
723 mBuffer = new byte[BUFFER_SIZE];
724 }
725 try {
726 InputStream is = mResolver.openInputStream(uri);
727 if (is != null) {
728 ByteArrayOutputStream baos = new ByteArrayOutputStream();
729 try {
730 int size;
731 while ((size = is.read(mBuffer)) != -1) {
732 baos.write(mBuffer, 0, size);
733 }
734 } finally {
735 is.close();
736 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800737 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700738 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700739 } else {
740 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800741 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700742 }
743 } catch (Exception ex) {
744 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800745 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700746 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800747 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800748 }
749 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800750}