blob: d8f4e652eff1773b7b98f27fedeae8f6f8d4e026 [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 /**
100 * Temporarily stops loading photos from the database.
101 */
102 public abstract void pause();
103
104 /**
105 * Resumes loading photos from the database.
106 */
107 public abstract void resume();
108
109 /**
110 * Marks all cached photos for reloading. We can continue using cache but should
111 * also make sure the photos haven't changed in the background and notify the views
112 * if so.
113 */
114 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800115
116 /**
117 * Initiates a background process that over time will fill up cache with
118 * preload photos.
119 */
120 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800121}
122
123class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800124 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
125
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800126 /**
127 * Type of message sent by the UI thread to itself to indicate that some photos
128 * need to be loaded.
129 */
130 private static final int MESSAGE_REQUEST_LOADING = 1;
131
132 /**
133 * Type of message sent by the loader thread to indicate that some photos have
134 * been loaded.
135 */
136 private static final int MESSAGE_PHOTOS_LOADED = 2;
137
138 private static final String[] EMPTY_STRING_ARRAY = new String[0];
139
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800140 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800141
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800142 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800143 * Maintains the state of a particular photo.
144 */
145 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800146 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800147
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800148 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800149 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800150 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800151
152 public BitmapHolder(byte[] bytes) {
153 this.bytes = bytes;
154 this.fresh = true;
155 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800156 }
157
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800158 private final Context mContext;
159
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800160 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800161 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800162 * as they come from the database. Each holder has a soft reference to the
163 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800164 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800165 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
166
167 /**
168 * Cache size threshold at which bitmaps will not be preloaded.
169 */
170 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800171
172 /**
173 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
174 * the most recently used bitmaps to save time on decoding
175 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
176 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800177 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800178
179 /**
180 * A map from ImageView to the corresponding photo ID. Please note that this
181 * photo ID may change before the photo loading request is started.
182 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700183 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
184 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800185
186 /**
187 * Handler for messages sent to the UI thread.
188 */
189 private final Handler mMainThreadHandler = new Handler(this);
190
191 /**
192 * Thread responsible for loading photos from the database. Created upon
193 * the first request.
194 */
195 private LoaderThread mLoaderThread;
196
197 /**
198 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
199 */
200 private boolean mLoadingRequested;
201
202 /**
203 * Flag indicating if the image loading is paused.
204 */
205 private boolean mPaused;
206
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800207 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800208 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800209
210 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800211 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800212 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800213 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
214 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
215 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800216 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800217 }
218 };
219 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800220 }
221
222 @Override
223 public void preloadPhotosInBackground() {
224 ensureLoaderThread();
225 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800226 }
227
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800228 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800229 public void loadPhoto(ImageView view, long photoId) {
230 if (photoId == 0) {
231 // No photo is needed
232 view.setImageResource(mDefaultResourceId);
233 mPendingRequests.remove(view);
234 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700235 loadPhotoByIdOrUri(view, photoId);
236 }
237 }
238
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800239 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700240 public void loadPhoto(ImageView view, Uri photoUri) {
241 if (photoUri == null) {
242 // No photo is needed
243 view.setImageResource(mDefaultResourceId);
244 mPendingRequests.remove(view);
245 } else {
246 loadPhotoByIdOrUri(view, photoUri);
247 }
248 }
249
250 private void loadPhotoByIdOrUri(ImageView view, Object key) {
251 boolean loaded = loadCachedPhoto(view, key);
252 if (loaded) {
253 mPendingRequests.remove(view);
254 } else {
255 mPendingRequests.put(view, key);
256 if (!mPaused) {
257 // Send a request to start loading photos
258 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800259 }
260 }
261 }
262
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800263 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800264 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800265 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800266 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800267 }
268 }
269
270 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800271 * Checks if the photo is present in cache. If so, sets the photo on the view.
272 *
273 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800274 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700275 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800276 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800277 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800278 // The bitmap has not been loaded - should display the placeholder image.
279 view.setImageResource(mDefaultResourceId);
280 return false;
281 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800282
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800283 if (holder.bytes == null) {
284 view.setImageResource(mDefaultResourceId);
285 return holder.fresh;
286 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800287
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800288 // Optionally decode bytes into a bitmap
289 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800290
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800291 view.setImageBitmap(holder.bitmap);
292
293 // Put the bitmap in the LRU cache
294 mBitmapCache.put(key, holder.bitmap);
295
296 // Soften the reference
297 holder.bitmap = null;
298
299 return holder.fresh;
300 }
301
302 /**
303 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
304 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
305 * the holder, it will not be necessary to decode the bitmap.
306 */
307 private void inflateBitmap(BitmapHolder holder) {
308 byte[] bytes = holder.bytes;
309 if (bytes == null || bytes.length == 0) {
310 return;
311 }
312
313 // Check the soft reference. If will be retained if the bitmap is also
314 // in the LRU cache, so we don't need to check the LRU cache explicitly.
315 if (holder.bitmapRef != null) {
316 holder.bitmap = holder.bitmapRef.get();
317 if (holder.bitmap != null) {
318 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800319 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800320 }
321
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800322 try {
323 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
324 holder.bitmap = bitmap;
325 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
326 } catch (OutOfMemoryError e) {
327 // Do nothing - the photo will appear to be missing
328 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800329 }
330
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700331 public void clear() {
332 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800333 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700334 }
335
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800336 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800337 public void pause() {
338 mPaused = true;
339 }
340
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800341 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800342 public void resume() {
343 mPaused = false;
344 if (!mPendingRequests.isEmpty()) {
345 requestLoading();
346 }
347 }
348
349 /**
350 * Sends a message to this thread itself to start loading images. If the current
351 * view contains multiple image views, all of those image views will get a chance
352 * to request their respective photos before any of those requests are executed.
353 * This allows us to load images in bulk.
354 */
355 private void requestLoading() {
356 if (!mLoadingRequested) {
357 mLoadingRequested = true;
358 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
359 }
360 }
361
362 /**
363 * Processes requests on the main thread.
364 */
365 public boolean handleMessage(Message msg) {
366 switch (msg.what) {
367 case MESSAGE_REQUEST_LOADING: {
368 mLoadingRequested = false;
369 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800370 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800371 mLoaderThread.requestLoading();
372 }
373 return true;
374 }
375
376 case MESSAGE_PHOTOS_LOADED: {
377 if (!mPaused) {
378 processLoadedImages();
379 }
380 return true;
381 }
382 }
383 return false;
384 }
385
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800386 public void ensureLoaderThread() {
387 if (mLoaderThread == null) {
388 mLoaderThread = new LoaderThread(mContext.getContentResolver());
389 mLoaderThread.start();
390 }
391 }
392
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800393 /**
394 * Goes over pending loading requests and displays loaded photos. If some of the
395 * photos still haven't been loaded, sends another request for image loading.
396 */
397 private void processLoadedImages() {
398 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
399 while (iterator.hasNext()) {
400 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700401 Object key = mPendingRequests.get(view);
402 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800403 if (loaded) {
404 iterator.remove();
405 }
406 }
407
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800408 softenCache();
409
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800410 if (!mPendingRequests.isEmpty()) {
411 requestLoading();
412 }
413 }
414
415 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800416 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800417 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800418 */
419 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800420 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800421 holder.bitmap = null;
422 }
423 }
424
425 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800426 * Stores the supplied bitmap in cache.
427 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800428 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
429 BitmapHolder holder = new BitmapHolder(bytes);
430 holder.fresh = true;
431
432 // Unless this image is being preloaded, decode it right away while
433 // we are still on the background thread.
434 if (!preloading) {
435 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800436 }
437
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800438 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800439 }
440
441 /**
442 * Populates an array of photo IDs that need to be loaded.
443 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100444 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
445 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800446 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800447 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700448 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800449
450 /*
451 * Since the call is made from the loader thread, the map could be
452 * changing during the iteration. That's not really a problem:
453 * ConcurrentHashMap will allow those changes to happen without throwing
454 * exceptions. Since we may miss some requests in the situation of
455 * concurrent change, we will need to check the map again once loading
456 * is complete.
457 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700458 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800459 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700460 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800461 BitmapHolder holder = mBitmapHolderCache.get(key);
462 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700463 if (key instanceof Long) {
464 photoIds.add((Long)key);
465 photoIdsAsStrings.add(key.toString());
466 } else {
467 uris.add((Uri)key);
468 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800469 }
470 }
471 }
472
473 /**
474 * The thread that performs loading of photos from the database.
475 */
476 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700477 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800478 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
479 private static final int MESSAGE_LOAD_PHOTOS = 1;
480
481 /**
482 * A pause between preload batches that yields to the UI thread.
483 */
484 private static final int PHOTO_PRELOAD_DELAY = 50;
485
486 /**
487 * Number of photos to preload per batch.
488 */
489 private static final int PRELOAD_BATCH = 25;
490
491 /**
492 * Maximum number of photos to preload. If the cache size is 2Mb and
493 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
494 */
495 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700496
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800497 private final ContentResolver mResolver;
498 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100499 private final Set<Long> mPhotoIds = Sets.newHashSet();
500 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
501 private final Set<Uri> mPhotoUris = Sets.newHashSet();
502 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800503
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800504 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700505 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800506
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800507 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
508 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
509 private static final int PRELOAD_STATUS_DONE = 2;
510
511 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
512
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800513 public LoaderThread(ContentResolver resolver) {
514 super(LOADER_THREAD_NAME);
515 mResolver = resolver;
516 }
517
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800518 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800519 if (mLoaderThreadHandler == null) {
520 mLoaderThreadHandler = new Handler(getLooper(), this);
521 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800522 }
523
524 /**
525 * Kicks off preloading of the next batch of photos on the background thread.
526 * Preloading will happen after a delay: we want to yield to the UI thread
527 * as much as possible.
528 * <p>
529 * If preloading is already complete, does nothing.
530 */
531 public void requestPreloading() {
532 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
533 return;
534 }
535
536 ensureHandler();
537 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
538 return;
539 }
540
541 mLoaderThreadHandler.sendEmptyMessageDelayed(
542 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
543 }
544
545 /**
546 * Sends a message to this thread to load requested photos. Cancels a preloading
547 * request, if any: we don't want preloading to impede loading of the photos
548 * we need to display now.
549 */
550 public void requestLoading() {
551 ensureHandler();
552 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
553 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800554 }
555
556 /**
557 * Receives the above message, loads photos and then sends a message
558 * to the main thread to process them.
559 */
560 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800561 switch (msg.what) {
562 case MESSAGE_PRELOAD_PHOTOS:
563 preloadPhotosInBackground();
564 break;
565 case MESSAGE_LOAD_PHOTOS:
566 loadPhotosInBackground();
567 break;
568 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800569 return true;
570 }
571
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800572 /**
573 * The first time it is called, figures out which photos need to be preloaded.
574 * Each subsequent call preloads the next batch of photos and requests
575 * another cycle of preloading after a delay. The whole process ends when
576 * we either run out of photos to preload or fill up cache.
577 */
578 private void preloadPhotosInBackground() {
579 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
580 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800581 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800582
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800583 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
584 queryPhotosForPreload();
585 if (mPreloadPhotoIds.isEmpty()) {
586 mPreloadStatus = PRELOAD_STATUS_DONE;
587 } else {
588 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
589 }
590 requestPreloading();
591 return;
592 }
593
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800594 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800595 mPreloadStatus = PRELOAD_STATUS_DONE;
596 return;
597 }
598
599 mPhotoIds.clear();
600 mPhotoIdsAsStrings.clear();
601
602 int count = 0;
603 int preloadSize = mPreloadPhotoIds.size();
604 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
605 preloadSize--;
606 count++;
607 Long photoId = mPreloadPhotoIds.get(preloadSize);
608 mPhotoIds.add(photoId);
609 mPhotoIdsAsStrings.add(photoId.toString());
610 mPreloadPhotoIds.remove(preloadSize);
611 }
612
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700613 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800614
615 if (preloadSize == 0) {
616 mPreloadStatus = PRELOAD_STATUS_DONE;
617 }
618
619 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
620 + mBitmapHolderCache.size()
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800621 + ". Total size: " + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800622
623 requestPreloading();
624 }
625
626 private void queryPhotosForPreload() {
627 Cursor cursor = null;
628 try {
629 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
630 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700631 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
632 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dave Santoro11a9df72011-06-30 11:33:28 -0700633 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1")
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800634 .build();
635 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
636 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
637 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700638 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800639
640 if (cursor != null) {
641 while (cursor.moveToNext()) {
642 // Insert them in reverse order, because we will be taking
643 // them from the end of the list for loading.
644 mPreloadPhotoIds.add(0, cursor.getLong(0));
645 }
646 }
647 } finally {
648 if (cursor != null) {
649 cursor.close();
650 }
651 }
652 }
653
654 private void loadPhotosInBackground() {
655 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700656 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800657 loadRemotePhotos();
658 requestPreloading();
659 }
660
661 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100662 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800663 return;
664 }
665
666 // Remove loaded photos from the preload queue: we don't want
667 // the preloading process to load them again.
668 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100669 for (Long id : mPhotoIds) {
670 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800671 }
672 if (mPreloadPhotoIds.isEmpty()) {
673 mPreloadStatus = PRELOAD_STATUS_DONE;
674 }
675 }
676
677 mStringBuilder.setLength(0);
678 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100679 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800680 if (i != 0) {
681 mStringBuilder.append(',');
682 }
683 mStringBuilder.append('?');
684 }
685 mStringBuilder.append(')');
686
687 Cursor cursor = null;
688 try {
Dave Santoro11a9df72011-06-30 11:33:28 -0700689 cursor = mResolver.query(Data.CONTENT_URI.buildUpon()
690 .appendQueryParameter(ContactsContract.ALLOW_PROFILE, "1").build(),
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800691 COLUMNS,
692 mStringBuilder.toString(),
693 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
694 null);
695
696 if (cursor != null) {
697 while (cursor.moveToNext()) {
698 Long id = cursor.getLong(0);
699 byte[] bytes = cursor.getBlob(1);
700 cacheBitmap(id, bytes, preloading);
701 mPhotoIds.remove(id);
702 }
703 }
704 } finally {
705 if (cursor != null) {
706 cursor.close();
707 }
708 }
709
710 // Remaining photos were not found in the database - mark the cache accordingly.
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100711 for (Long id : mPhotoIds) {
712 cacheBitmap(id, null, preloading);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800713 }
714
715 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
716 }
717
718 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100719 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700720 if (mBuffer == null) {
721 mBuffer = new byte[BUFFER_SIZE];
722 }
723 try {
724 InputStream is = mResolver.openInputStream(uri);
725 if (is != null) {
726 ByteArrayOutputStream baos = new ByteArrayOutputStream();
727 try {
728 int size;
729 while ((size = is.read(mBuffer)) != -1) {
730 baos.write(mBuffer, 0, size);
731 }
732 } finally {
733 is.close();
734 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800735 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700736 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700737 } else {
738 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800739 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700740 }
741 } catch (Exception ex) {
742 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800743 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700744 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800745 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800746 }
747 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800748}