blob: fd2e6a21a4ca3517817c9394a88858f5b66d40d1 [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;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080046import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080047import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010048import java.util.List;
49import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080050import java.util.concurrent.ConcurrentHashMap;
51
52/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080053 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080054 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080055public abstract class ContactPhotoManager {
56
57 static final String TAG = "ContactPhotoManager";
58
59 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
60
61 /**
62 * The resource ID of the image to be used when the photo is unavailable or being
63 * loaded.
64 */
65 protected final int mDefaultResourceId = R.drawable.ic_contact_picture;
66
67 /**
68 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
69 * the available authenticators. This method can safely be called from the UI thread.
70 */
71 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010072 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080073 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +010074 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080075 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010076 service = createContactPhotoManager(applicationContext);
77 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080078 }
79 return service;
80 }
81
82 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
83 return new ContactPhotoManagerImpl(context);
84 }
85
86 /**
87 * Load photo into the supplied image view. If the photo is already cached,
88 * it is displayed immediately. Otherwise a request is sent to load the photo
89 * from the database.
90 */
91 public abstract void loadPhoto(ImageView view, long photoId);
92
93 /**
94 * Load photo into the supplied image view. If the photo is already cached,
95 * it is displayed immediately. Otherwise a request is sent to load the photo
96 * from the location specified by the URI.
97 */
98 public abstract void loadPhoto(ImageView view, Uri photoUri);
99
100 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700101 * Remove photo from the supplied image view. This also cancels current pending load request
102 * inside this photo manager.
103 */
104 public abstract void removePhoto(ImageView view);
105
106 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800107 * Temporarily stops loading photos from the database.
108 */
109 public abstract void pause();
110
111 /**
112 * Resumes loading photos from the database.
113 */
114 public abstract void resume();
115
116 /**
117 * Marks all cached photos for reloading. We can continue using cache but should
118 * also make sure the photos haven't changed in the background and notify the views
119 * if so.
120 */
121 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800122
123 /**
124 * Initiates a background process that over time will fill up cache with
125 * preload photos.
126 */
127 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800128}
129
130class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800131 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
132
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800133 /**
134 * Type of message sent by the UI thread to itself to indicate that some photos
135 * need to be loaded.
136 */
137 private static final int MESSAGE_REQUEST_LOADING = 1;
138
139 /**
140 * Type of message sent by the loader thread to indicate that some photos have
141 * been loaded.
142 */
143 private static final int MESSAGE_PHOTOS_LOADED = 2;
144
145 private static final String[] EMPTY_STRING_ARRAY = new String[0];
146
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800147 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800148
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800149 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800150 * Maintains the state of a particular photo.
151 */
152 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800153 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800154
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800155 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800156 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800157 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800158
159 public BitmapHolder(byte[] bytes) {
160 this.bytes = bytes;
161 this.fresh = true;
162 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800163 }
164
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800165 private final Context mContext;
166
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800167 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800168 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800169 * as they come from the database. Each holder has a soft reference to the
170 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800171 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800172 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
173
174 /**
175 * Cache size threshold at which bitmaps will not be preloaded.
176 */
177 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800178
179 /**
180 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
181 * the most recently used bitmaps to save time on decoding
182 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
183 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800184 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800185
186 /**
187 * A map from ImageView to the corresponding photo ID. Please note that this
188 * photo ID may change before the photo loading request is started.
189 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700190 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
191 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800192
193 /**
194 * Handler for messages sent to the UI thread.
195 */
196 private final Handler mMainThreadHandler = new Handler(this);
197
198 /**
199 * Thread responsible for loading photos from the database. Created upon
200 * the first request.
201 */
202 private LoaderThread mLoaderThread;
203
204 /**
205 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
206 */
207 private boolean mLoadingRequested;
208
209 /**
210 * Flag indicating if the image loading is paused.
211 */
212 private boolean mPaused;
213
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800214 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800215 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800216
217 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800218 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800219 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800220 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
221 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
222 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800223 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800224 }
225 };
226 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800227 }
228
229 @Override
230 public void preloadPhotosInBackground() {
231 ensureLoaderThread();
232 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800233 }
234
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800235 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800236 public void loadPhoto(ImageView view, long photoId) {
237 if (photoId == 0) {
238 // No photo is needed
239 view.setImageResource(mDefaultResourceId);
240 mPendingRequests.remove(view);
241 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700242 loadPhotoByIdOrUri(view, photoId);
243 }
244 }
245
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800246 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700247 public void loadPhoto(ImageView view, Uri photoUri) {
248 if (photoUri == null) {
249 // No photo is needed
250 view.setImageResource(mDefaultResourceId);
251 mPendingRequests.remove(view);
252 } else {
253 loadPhotoByIdOrUri(view, photoUri);
254 }
255 }
256
257 private void loadPhotoByIdOrUri(ImageView view, Object key) {
258 boolean loaded = loadCachedPhoto(view, key);
259 if (loaded) {
260 mPendingRequests.remove(view);
261 } else {
262 mPendingRequests.put(view, key);
263 if (!mPaused) {
264 // Send a request to start loading photos
265 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800266 }
267 }
268 }
269
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800270 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700271 public void removePhoto(ImageView view) {
272 view.setImageDrawable(null);
273 mPendingRequests.remove(view);
274 }
275
276 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800277 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800278 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800279 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800280 }
281 }
282
283 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800284 * Checks if the photo is present in cache. If so, sets the photo on the view.
285 *
286 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800287 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700288 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800289 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800290 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800291 // The bitmap has not been loaded - should display the placeholder image.
292 view.setImageResource(mDefaultResourceId);
293 return false;
294 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800295
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800296 if (holder.bytes == null) {
297 view.setImageResource(mDefaultResourceId);
298 return holder.fresh;
299 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800300
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800301 // Optionally decode bytes into a bitmap
302 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800303
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800304 view.setImageBitmap(holder.bitmap);
305
306 // Put the bitmap in the LRU cache
307 mBitmapCache.put(key, holder.bitmap);
308
309 // Soften the reference
310 holder.bitmap = null;
311
312 return holder.fresh;
313 }
314
315 /**
316 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
317 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
318 * the holder, it will not be necessary to decode the bitmap.
319 */
320 private void inflateBitmap(BitmapHolder holder) {
321 byte[] bytes = holder.bytes;
322 if (bytes == null || bytes.length == 0) {
323 return;
324 }
325
326 // Check the soft reference. If will be retained if the bitmap is also
327 // in the LRU cache, so we don't need to check the LRU cache explicitly.
328 if (holder.bitmapRef != null) {
329 holder.bitmap = holder.bitmapRef.get();
330 if (holder.bitmap != null) {
331 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800332 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800333 }
334
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800335 try {
336 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
337 holder.bitmap = bitmap;
338 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
339 } catch (OutOfMemoryError e) {
340 // Do nothing - the photo will appear to be missing
341 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800342 }
343
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700344 public void clear() {
345 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800346 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700347 }
348
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800349 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800350 public void pause() {
351 mPaused = true;
352 }
353
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800354 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800355 public void resume() {
356 mPaused = false;
357 if (!mPendingRequests.isEmpty()) {
358 requestLoading();
359 }
360 }
361
362 /**
363 * Sends a message to this thread itself to start loading images. If the current
364 * view contains multiple image views, all of those image views will get a chance
365 * to request their respective photos before any of those requests are executed.
366 * This allows us to load images in bulk.
367 */
368 private void requestLoading() {
369 if (!mLoadingRequested) {
370 mLoadingRequested = true;
371 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
372 }
373 }
374
375 /**
376 * Processes requests on the main thread.
377 */
378 public boolean handleMessage(Message msg) {
379 switch (msg.what) {
380 case MESSAGE_REQUEST_LOADING: {
381 mLoadingRequested = false;
382 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800383 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800384 mLoaderThread.requestLoading();
385 }
386 return true;
387 }
388
389 case MESSAGE_PHOTOS_LOADED: {
390 if (!mPaused) {
391 processLoadedImages();
392 }
393 return true;
394 }
395 }
396 return false;
397 }
398
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800399 public void ensureLoaderThread() {
400 if (mLoaderThread == null) {
401 mLoaderThread = new LoaderThread(mContext.getContentResolver());
402 mLoaderThread.start();
403 }
404 }
405
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800406 /**
407 * Goes over pending loading requests and displays loaded photos. If some of the
408 * photos still haven't been loaded, sends another request for image loading.
409 */
410 private void processLoadedImages() {
411 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
412 while (iterator.hasNext()) {
413 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700414 Object key = mPendingRequests.get(view);
415 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800416 if (loaded) {
417 iterator.remove();
418 }
419 }
420
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800421 softenCache();
422
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800423 if (!mPendingRequests.isEmpty()) {
424 requestLoading();
425 }
426 }
427
428 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800429 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800430 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800431 */
432 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800433 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800434 holder.bitmap = null;
435 }
436 }
437
438 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800439 * Stores the supplied bitmap in cache.
440 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800441 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
442 BitmapHolder holder = new BitmapHolder(bytes);
443 holder.fresh = true;
444
445 // Unless this image is being preloaded, decode it right away while
446 // we are still on the background thread.
447 if (!preloading) {
448 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800449 }
450
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800451 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800452 }
453
454 /**
455 * Populates an array of photo IDs that need to be loaded.
456 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100457 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
458 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800459 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800460 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700461 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800462
463 /*
464 * Since the call is made from the loader thread, the map could be
465 * changing during the iteration. That's not really a problem:
466 * ConcurrentHashMap will allow those changes to happen without throwing
467 * exceptions. Since we may miss some requests in the situation of
468 * concurrent change, we will need to check the map again once loading
469 * is complete.
470 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700471 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800472 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700473 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800474 BitmapHolder holder = mBitmapHolderCache.get(key);
475 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700476 if (key instanceof Long) {
477 photoIds.add((Long)key);
478 photoIdsAsStrings.add(key.toString());
479 } else {
480 uris.add((Uri)key);
481 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800482 }
483 }
484 }
485
486 /**
487 * The thread that performs loading of photos from the database.
488 */
489 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700490 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800491 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
492 private static final int MESSAGE_LOAD_PHOTOS = 1;
493
494 /**
495 * A pause between preload batches that yields to the UI thread.
496 */
497 private static final int PHOTO_PRELOAD_DELAY = 50;
498
499 /**
500 * Number of photos to preload per batch.
501 */
502 private static final int PRELOAD_BATCH = 25;
503
504 /**
505 * Maximum number of photos to preload. If the cache size is 2Mb and
506 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
507 */
508 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700509
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800510 private final ContentResolver mResolver;
511 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100512 private final Set<Long> mPhotoIds = Sets.newHashSet();
513 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
514 private final Set<Uri> mPhotoUris = Sets.newHashSet();
515 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800516
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800517 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700518 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800519
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800520 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
521 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
522 private static final int PRELOAD_STATUS_DONE = 2;
523
524 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
525
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800526 public LoaderThread(ContentResolver resolver) {
527 super(LOADER_THREAD_NAME);
528 mResolver = resolver;
529 }
530
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800531 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800532 if (mLoaderThreadHandler == null) {
533 mLoaderThreadHandler = new Handler(getLooper(), this);
534 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800535 }
536
537 /**
538 * Kicks off preloading of the next batch of photos on the background thread.
539 * Preloading will happen after a delay: we want to yield to the UI thread
540 * as much as possible.
541 * <p>
542 * If preloading is already complete, does nothing.
543 */
544 public void requestPreloading() {
545 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
546 return;
547 }
548
549 ensureHandler();
550 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
551 return;
552 }
553
554 mLoaderThreadHandler.sendEmptyMessageDelayed(
555 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
556 }
557
558 /**
559 * Sends a message to this thread to load requested photos. Cancels a preloading
560 * request, if any: we don't want preloading to impede loading of the photos
561 * we need to display now.
562 */
563 public void requestLoading() {
564 ensureHandler();
565 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
566 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800567 }
568
569 /**
570 * Receives the above message, loads photos and then sends a message
571 * to the main thread to process them.
572 */
573 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800574 switch (msg.what) {
575 case MESSAGE_PRELOAD_PHOTOS:
576 preloadPhotosInBackground();
577 break;
578 case MESSAGE_LOAD_PHOTOS:
579 loadPhotosInBackground();
580 break;
581 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800582 return true;
583 }
584
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800585 /**
586 * The first time it is called, figures out which photos need to be preloaded.
587 * Each subsequent call preloads the next batch of photos and requests
588 * another cycle of preloading after a delay. The whole process ends when
589 * we either run out of photos to preload or fill up cache.
590 */
591 private void preloadPhotosInBackground() {
592 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
593 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800594 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800595
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800596 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
597 queryPhotosForPreload();
598 if (mPreloadPhotoIds.isEmpty()) {
599 mPreloadStatus = PRELOAD_STATUS_DONE;
600 } else {
601 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
602 }
603 requestPreloading();
604 return;
605 }
606
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800607 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800608 mPreloadStatus = PRELOAD_STATUS_DONE;
609 return;
610 }
611
612 mPhotoIds.clear();
613 mPhotoIdsAsStrings.clear();
614
615 int count = 0;
616 int preloadSize = mPreloadPhotoIds.size();
617 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
618 preloadSize--;
619 count++;
620 Long photoId = mPreloadPhotoIds.get(preloadSize);
621 mPhotoIds.add(photoId);
622 mPhotoIdsAsStrings.add(photoId.toString());
623 mPreloadPhotoIds.remove(preloadSize);
624 }
625
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700626 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800627
628 if (preloadSize == 0) {
629 mPreloadStatus = PRELOAD_STATUS_DONE;
630 }
631
632 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
633 + mBitmapHolderCache.size()
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800634 + ". Total size: " + 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}