blob: eb9531a1d692ed0882b0ada25d9ff1159d4a9ba7 [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;
Dave Santoro84cac442011-08-24 15:23:10 -070024import android.content.ContentUris;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080025import android.content.Context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080026import android.content.res.Resources;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080027import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070030import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080031import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070032import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080033import android.os.HandlerThread;
34import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080035import android.provider.ContactsContract;
36import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080037import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070038import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080039import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070040import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080041import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080042import android.widget.ImageView;
43
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070044import java.io.ByteArrayOutputStream;
45import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070046import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080047import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080048import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010049import java.util.List;
50import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080051import java.util.concurrent.ConcurrentHashMap;
52
53/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080054 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080055 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080056public abstract class ContactPhotoManager {
57
58 static final String TAG = "ContactPhotoManager";
59
60 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
61
62 /**
63 * The resource ID of the image to be used when the photo is unavailable or being
64 * loaded.
65 */
66 protected final int mDefaultResourceId = R.drawable.ic_contact_picture;
67
68 /**
69 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
70 * the available authenticators. This method can safely be called from the UI thread.
71 */
72 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010073 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080074 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +010075 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080076 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010077 service = createContactPhotoManager(applicationContext);
78 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080079 }
80 return service;
81 }
82
83 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
84 return new ContactPhotoManagerImpl(context);
85 }
86
87 /**
88 * Load photo into the supplied image view. If the photo is already cached,
89 * it is displayed immediately. Otherwise a request is sent to load the photo
90 * from the database.
91 */
92 public abstract void loadPhoto(ImageView view, long photoId);
93
94 /**
95 * Load photo into the supplied image view. If the photo is already cached,
96 * it is displayed immediately. Otherwise a request is sent to load the photo
97 * from the location specified by the URI.
98 */
99 public abstract void loadPhoto(ImageView view, Uri photoUri);
100
101 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700102 * Remove photo from the supplied image view. This also cancels current pending load request
103 * inside this photo manager.
104 */
105 public abstract void removePhoto(ImageView view);
106
107 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800108 * Temporarily stops loading photos from the database.
109 */
110 public abstract void pause();
111
112 /**
113 * Resumes loading photos from the database.
114 */
115 public abstract void resume();
116
117 /**
118 * Marks all cached photos for reloading. We can continue using cache but should
119 * also make sure the photos haven't changed in the background and notify the views
120 * if so.
121 */
122 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800123
124 /**
125 * Initiates a background process that over time will fill up cache with
126 * preload photos.
127 */
128 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800129}
130
131class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800132 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
133
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800134 /**
135 * Type of message sent by the UI thread to itself to indicate that some photos
136 * need to be loaded.
137 */
138 private static final int MESSAGE_REQUEST_LOADING = 1;
139
140 /**
141 * Type of message sent by the loader thread to indicate that some photos have
142 * been loaded.
143 */
144 private static final int MESSAGE_PHOTOS_LOADED = 2;
145
146 private static final String[] EMPTY_STRING_ARRAY = new String[0];
147
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800148 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800149
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800150 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800151 * Maintains the state of a particular photo.
152 */
153 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800154 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800155
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800156 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800157 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700158 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800159
160 public BitmapHolder(byte[] bytes) {
161 this.bytes = bytes;
162 this.fresh = true;
163 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800164 }
165
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800166 private final Context mContext;
167
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800168 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800169 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800170 * as they come from the database. Each holder has a soft reference to the
171 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800172 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800173 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
174
175 /**
176 * Cache size threshold at which bitmaps will not be preloaded.
177 */
178 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800179
180 /**
181 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
182 * the most recently used bitmaps to save time on decoding
183 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
184 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800185 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800186
187 /**
188 * A map from ImageView to the corresponding photo ID. Please note that this
189 * photo ID may change before the photo loading request is started.
190 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700191 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
192 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800193
194 /**
195 * Handler for messages sent to the UI thread.
196 */
197 private final Handler mMainThreadHandler = new Handler(this);
198
199 /**
200 * Thread responsible for loading photos from the database. Created upon
201 * the first request.
202 */
203 private LoaderThread mLoaderThread;
204
205 /**
206 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
207 */
208 private boolean mLoadingRequested;
209
210 /**
211 * Flag indicating if the image loading is paused.
212 */
213 private boolean mPaused;
214
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800215 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800216 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800217
218 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800219 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800220 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800221 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
222 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
223 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800224 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800225 }
226 };
227 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800228 }
229
230 @Override
231 public void preloadPhotosInBackground() {
232 ensureLoaderThread();
233 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800234 }
235
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800236 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800237 public void loadPhoto(ImageView view, long photoId) {
238 if (photoId == 0) {
239 // No photo is needed
240 view.setImageResource(mDefaultResourceId);
241 mPendingRequests.remove(view);
242 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700243 loadPhotoByIdOrUri(view, photoId);
244 }
245 }
246
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800247 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700248 public void loadPhoto(ImageView view, Uri photoUri) {
249 if (photoUri == null) {
250 // No photo is needed
251 view.setImageResource(mDefaultResourceId);
252 mPendingRequests.remove(view);
253 } else {
254 loadPhotoByIdOrUri(view, photoUri);
255 }
256 }
257
258 private void loadPhotoByIdOrUri(ImageView view, Object key) {
259 boolean loaded = loadCachedPhoto(view, key);
260 if (loaded) {
261 mPendingRequests.remove(view);
262 } else {
263 mPendingRequests.put(view, key);
264 if (!mPaused) {
265 // Send a request to start loading photos
266 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800267 }
268 }
269 }
270
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800271 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700272 public void removePhoto(ImageView view) {
273 view.setImageDrawable(null);
274 mPendingRequests.remove(view);
275 }
276
277 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800278 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800279 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800280 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800281 }
282 }
283
284 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800285 * Checks if the photo is present in cache. If so, sets the photo on the view.
286 *
287 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800288 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700289 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800290 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800291 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800292 // The bitmap has not been loaded - should display the placeholder image.
293 view.setImageResource(mDefaultResourceId);
294 return false;
295 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800296
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800297 if (holder.bytes == null) {
298 view.setImageResource(mDefaultResourceId);
299 return holder.fresh;
300 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800301
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800302 // Optionally decode bytes into a bitmap
303 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800304
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800305 view.setImageBitmap(holder.bitmap);
306
307 // Put the bitmap in the LRU cache
308 mBitmapCache.put(key, holder.bitmap);
309
310 // Soften the reference
311 holder.bitmap = null;
312
313 return holder.fresh;
314 }
315
316 /**
317 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
318 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
319 * the holder, it will not be necessary to decode the bitmap.
320 */
321 private void inflateBitmap(BitmapHolder holder) {
322 byte[] bytes = holder.bytes;
323 if (bytes == null || bytes.length == 0) {
324 return;
325 }
326
327 // Check the soft reference. If will be retained if the bitmap is also
328 // in the LRU cache, so we don't need to check the LRU cache explicitly.
329 if (holder.bitmapRef != null) {
330 holder.bitmap = holder.bitmapRef.get();
331 if (holder.bitmap != null) {
332 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800333 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800334 }
335
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800336 try {
337 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
338 holder.bitmap = bitmap;
339 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
340 } catch (OutOfMemoryError e) {
341 // Do nothing - the photo will appear to be missing
342 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800343 }
344
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700345 public void clear() {
346 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800347 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700348 }
349
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800350 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800351 public void pause() {
352 mPaused = true;
353 }
354
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800355 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800356 public void resume() {
357 mPaused = false;
358 if (!mPendingRequests.isEmpty()) {
359 requestLoading();
360 }
361 }
362
363 /**
364 * Sends a message to this thread itself to start loading images. If the current
365 * view contains multiple image views, all of those image views will get a chance
366 * to request their respective photos before any of those requests are executed.
367 * This allows us to load images in bulk.
368 */
369 private void requestLoading() {
370 if (!mLoadingRequested) {
371 mLoadingRequested = true;
372 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
373 }
374 }
375
376 /**
377 * Processes requests on the main thread.
378 */
379 public boolean handleMessage(Message msg) {
380 switch (msg.what) {
381 case MESSAGE_REQUEST_LOADING: {
382 mLoadingRequested = false;
383 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800384 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800385 mLoaderThread.requestLoading();
386 }
387 return true;
388 }
389
390 case MESSAGE_PHOTOS_LOADED: {
391 if (!mPaused) {
392 processLoadedImages();
393 }
394 return true;
395 }
396 }
397 return false;
398 }
399
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800400 public void ensureLoaderThread() {
401 if (mLoaderThread == null) {
402 mLoaderThread = new LoaderThread(mContext.getContentResolver());
403 mLoaderThread.start();
404 }
405 }
406
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800407 /**
408 * Goes over pending loading requests and displays loaded photos. If some of the
409 * photos still haven't been loaded, sends another request for image loading.
410 */
411 private void processLoadedImages() {
412 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
413 while (iterator.hasNext()) {
414 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700415 Object key = mPendingRequests.get(view);
416 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800417 if (loaded) {
418 iterator.remove();
419 }
420 }
421
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800422 softenCache();
423
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800424 if (!mPendingRequests.isEmpty()) {
425 requestLoading();
426 }
427 }
428
429 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800430 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800431 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800432 */
433 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800434 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800435 holder.bitmap = null;
436 }
437 }
438
439 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800440 * Stores the supplied bitmap in cache.
441 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800442 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
443 BitmapHolder holder = new BitmapHolder(bytes);
444 holder.fresh = true;
445
446 // Unless this image is being preloaded, decode it right away while
447 // we are still on the background thread.
448 if (!preloading) {
449 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800450 }
451
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800452 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800453 }
454
455 /**
456 * Populates an array of photo IDs that need to be loaded.
457 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100458 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
459 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800460 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800461 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700462 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800463
464 /*
465 * Since the call is made from the loader thread, the map could be
466 * changing during the iteration. That's not really a problem:
467 * ConcurrentHashMap will allow those changes to happen without throwing
468 * exceptions. Since we may miss some requests in the situation of
469 * concurrent change, we will need to check the map again once loading
470 * is complete.
471 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700472 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800473 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700474 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800475 BitmapHolder holder = mBitmapHolderCache.get(key);
476 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700477 if (key instanceof Long) {
478 photoIds.add((Long)key);
479 photoIdsAsStrings.add(key.toString());
480 } else {
481 uris.add((Uri)key);
482 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800483 }
484 }
485 }
486
487 /**
488 * The thread that performs loading of photos from the database.
489 */
490 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700491 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800492 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
493 private static final int MESSAGE_LOAD_PHOTOS = 1;
494
495 /**
496 * A pause between preload batches that yields to the UI thread.
497 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700498 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800499
500 /**
501 * Number of photos to preload per batch.
502 */
503 private static final int PRELOAD_BATCH = 25;
504
505 /**
506 * Maximum number of photos to preload. If the cache size is 2Mb and
507 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
508 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700509 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700510
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800511 private final ContentResolver mResolver;
512 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100513 private final Set<Long> mPhotoIds = Sets.newHashSet();
514 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
515 private final Set<Uri> mPhotoUris = Sets.newHashSet();
516 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800517
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800518 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700519 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800520
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800521 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
522 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
523 private static final int PRELOAD_STATUS_DONE = 2;
524
525 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
526
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800527 public LoaderThread(ContentResolver resolver) {
528 super(LOADER_THREAD_NAME);
529 mResolver = resolver;
530 }
531
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800532 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800533 if (mLoaderThreadHandler == null) {
534 mLoaderThreadHandler = new Handler(getLooper(), this);
535 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800536 }
537
538 /**
539 * Kicks off preloading of the next batch of photos on the background thread.
540 * Preloading will happen after a delay: we want to yield to the UI thread
541 * as much as possible.
542 * <p>
543 * If preloading is already complete, does nothing.
544 */
545 public void requestPreloading() {
546 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
547 return;
548 }
549
550 ensureHandler();
551 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
552 return;
553 }
554
555 mLoaderThreadHandler.sendEmptyMessageDelayed(
556 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
557 }
558
559 /**
560 * Sends a message to this thread to load requested photos. Cancels a preloading
561 * request, if any: we don't want preloading to impede loading of the photos
562 * we need to display now.
563 */
564 public void requestLoading() {
565 ensureHandler();
566 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
567 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800568 }
569
570 /**
571 * Receives the above message, loads photos and then sends a message
572 * to the main thread to process them.
573 */
574 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800575 switch (msg.what) {
576 case MESSAGE_PRELOAD_PHOTOS:
577 preloadPhotosInBackground();
578 break;
579 case MESSAGE_LOAD_PHOTOS:
580 loadPhotosInBackground();
581 break;
582 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800583 return true;
584 }
585
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800586 /**
587 * The first time it is called, figures out which photos need to be preloaded.
588 * Each subsequent call preloads the next batch of photos and requests
589 * another cycle of preloading after a delay. The whole process ends when
590 * we either run out of photos to preload or fill up cache.
591 */
592 private void preloadPhotosInBackground() {
593 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
594 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800595 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800596
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800597 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
598 queryPhotosForPreload();
599 if (mPreloadPhotoIds.isEmpty()) {
600 mPreloadStatus = PRELOAD_STATUS_DONE;
601 } else {
602 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
603 }
604 requestPreloading();
605 return;
606 }
607
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800608 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800609 mPreloadStatus = PRELOAD_STATUS_DONE;
610 return;
611 }
612
613 mPhotoIds.clear();
614 mPhotoIdsAsStrings.clear();
615
616 int count = 0;
617 int preloadSize = mPreloadPhotoIds.size();
618 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
619 preloadSize--;
620 count++;
621 Long photoId = mPreloadPhotoIds.get(preloadSize);
622 mPhotoIds.add(photoId);
623 mPhotoIdsAsStrings.add(photoId.toString());
624 mPreloadPhotoIds.remove(preloadSize);
625 }
626
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700627 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800628
629 if (preloadSize == 0) {
630 mPreloadStatus = PRELOAD_STATUS_DONE;
631 }
632
Makoto Onuki173f2812011-09-06 14:49:27 -0700633 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
634 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800635
636 requestPreloading();
637 }
638
639 private void queryPhotosForPreload() {
640 Cursor cursor = null;
641 try {
642 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
643 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700644 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
645 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800646 .build();
647 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
648 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
649 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700650 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800651
652 if (cursor != null) {
653 while (cursor.moveToNext()) {
654 // Insert them in reverse order, because we will be taking
655 // them from the end of the list for loading.
656 mPreloadPhotoIds.add(0, cursor.getLong(0));
657 }
658 }
659 } finally {
660 if (cursor != null) {
661 cursor.close();
662 }
663 }
664 }
665
666 private void loadPhotosInBackground() {
667 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700668 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800669 loadRemotePhotos();
670 requestPreloading();
671 }
672
673 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100674 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800675 return;
676 }
677
678 // Remove loaded photos from the preload queue: we don't want
679 // the preloading process to load them again.
680 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100681 for (Long id : mPhotoIds) {
682 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800683 }
684 if (mPreloadPhotoIds.isEmpty()) {
685 mPreloadStatus = PRELOAD_STATUS_DONE;
686 }
687 }
688
689 mStringBuilder.setLength(0);
690 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100691 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800692 if (i != 0) {
693 mStringBuilder.append(',');
694 }
695 mStringBuilder.append('?');
696 }
697 mStringBuilder.append(')');
698
699 Cursor cursor = null;
700 try {
Dave Santoro84cac442011-08-24 15:23:10 -0700701 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800702 COLUMNS,
703 mStringBuilder.toString(),
704 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
705 null);
706
707 if (cursor != null) {
708 while (cursor.moveToNext()) {
709 Long id = cursor.getLong(0);
710 byte[] bytes = cursor.getBlob(1);
711 cacheBitmap(id, bytes, preloading);
712 mPhotoIds.remove(id);
713 }
714 }
715 } finally {
716 if (cursor != null) {
717 cursor.close();
718 }
719 }
720
Dave Santoro84cac442011-08-24 15:23:10 -0700721 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100722 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700723 if (ContactsContract.isProfileId(id)) {
724 Cursor profileCursor = null;
725 try {
726 profileCursor = mResolver.query(
727 ContentUris.withAppendedId(Data.CONTENT_URI, id),
728 COLUMNS, null, null, null);
729 if (profileCursor != null && profileCursor.moveToFirst()) {
730 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
731 preloading);
732 } else {
733 // Couldn't load a photo this way either.
734 cacheBitmap(id, null, preloading);
735 }
736 } finally {
737 if (profileCursor != null) {
738 profileCursor.close();
739 }
740 }
741 } else {
742 // Not a profile photo and not found - mark the cache accordingly
743 cacheBitmap(id, null, preloading);
744 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800745 }
746
747 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
748 }
749
750 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100751 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700752 if (mBuffer == null) {
753 mBuffer = new byte[BUFFER_SIZE];
754 }
755 try {
756 InputStream is = mResolver.openInputStream(uri);
757 if (is != null) {
758 ByteArrayOutputStream baos = new ByteArrayOutputStream();
759 try {
760 int size;
761 while ((size = is.read(mBuffer)) != -1) {
762 baos.write(mBuffer, 0, size);
763 }
764 } finally {
765 is.close();
766 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800767 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700768 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700769 } else {
770 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800771 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700772 }
773 } catch (Exception ex) {
774 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800775 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700776 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800777 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800778 }
779 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800780}