blob: 4df7bbb752e5213fc75c3964fff9e8615e5900ff [file] [log] [blame]
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Dmitri Plotnikov022b62d2011-01-28 12:16:07 -080017package com.android.contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080018
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080019import com.android.contacts.model.AccountTypeManager;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080020import com.google.android.collect.Lists;
21
22import android.content.ContentResolver;
23import android.content.Context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080024import android.content.res.Resources;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080025import android.database.Cursor;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070028import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080029import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070030import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080031import android.os.HandlerThread;
32import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080033import android.provider.ContactsContract;
34import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080035import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070036import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080037import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070038import android.util.Log;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080039import android.widget.ImageView;
40
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070041import java.io.ByteArrayOutputStream;
42import java.io.InputStream;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080043import java.lang.ref.SoftReference;
44import java.util.ArrayList;
45import java.util.Iterator;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080046import java.util.LinkedHashMap;
47import java.util.Map;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080048import java.util.concurrent.ConcurrentHashMap;
49
50/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080051 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080052 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080053public abstract class ContactPhotoManager {
54
55 static final String TAG = "ContactPhotoManager";
56
57 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
58
59 /**
60 * The resource ID of the image to be used when the photo is unavailable or being
61 * loaded.
62 */
63 protected final int mDefaultResourceId = R.drawable.ic_contact_picture;
64
65 /**
66 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
67 * the available authenticators. This method can safely be called from the UI thread.
68 */
69 public static ContactPhotoManager getInstance(Context context) {
70 ContactPhotoManager service =
71 (ContactPhotoManager) context.getSystemService(CONTACT_PHOTO_SERVICE);
72 if (service == null) {
73 service = createContactPhotoManager(context);
74 Log.e(TAG, "No contact photo service in context: " + context);
75 }
76 return service;
77 }
78
79 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
80 return new ContactPhotoManagerImpl(context);
81 }
82
83 /**
84 * Load photo into the supplied image view. If the photo is already cached,
85 * it is displayed immediately. Otherwise a request is sent to load the photo
86 * from the database.
87 */
88 public abstract void loadPhoto(ImageView view, long photoId);
89
90 /**
91 * Load photo into the supplied image view. If the photo is already cached,
92 * it is displayed immediately. Otherwise a request is sent to load the photo
93 * from the location specified by the URI.
94 */
95 public abstract void loadPhoto(ImageView view, Uri photoUri);
96
97 /**
98 * Temporarily stops loading photos from the database.
99 */
100 public abstract void pause();
101
102 /**
103 * Resumes loading photos from the database.
104 */
105 public abstract void resume();
106
107 /**
108 * Marks all cached photos for reloading. We can continue using cache but should
109 * also make sure the photos haven't changed in the background and notify the views
110 * if so.
111 */
112 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800113
114 /**
115 * Initiates a background process that over time will fill up cache with
116 * preload photos.
117 */
118 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800119}
120
121class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800122 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
123
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800124 /**
125 * Type of message sent by the UI thread to itself to indicate that some photos
126 * need to be loaded.
127 */
128 private static final int MESSAGE_REQUEST_LOADING = 1;
129
130 /**
131 * Type of message sent by the loader thread to indicate that some photos have
132 * been loaded.
133 */
134 private static final int MESSAGE_PHOTOS_LOADED = 2;
135
136 private static final String[] EMPTY_STRING_ARRAY = new String[0];
137
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800138 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800139
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800140 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800141 * Maintains the state of a particular photo.
142 */
143 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800144 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800145
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800146 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800147 Bitmap bitmap;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800148 SoftReference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800149
150 public BitmapHolder(byte[] bytes) {
151 this.bytes = bytes;
152 this.fresh = true;
153 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800154 }
155
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800156 private final Context mContext;
157
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800158 private static final int BITMAP_CACHE_INITIAL_CAPACITY = 32;
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
162 * as they come from the database. It also softly retains the actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800163 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800164 private final BitmapHolderCache mBitmapHolderCache;
165
166 /**
167 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
168 * the most recently used bitmaps to save time on decoding
169 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
170 */
171 private final BitmapCache mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800172
173 /**
174 * A map from ImageView to the corresponding photo ID. Please note that this
175 * photo ID may change before the photo loading request is started.
176 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700177 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
178 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800179
180 /**
181 * Handler for messages sent to the UI thread.
182 */
183 private final Handler mMainThreadHandler = new Handler(this);
184
185 /**
186 * Thread responsible for loading photos from the database. Created upon
187 * the first request.
188 */
189 private LoaderThread mLoaderThread;
190
191 /**
192 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
193 */
194 private boolean mLoadingRequested;
195
196 /**
197 * Flag indicating if the image loading is paused.
198 */
199 private boolean mPaused;
200
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800201 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800202 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800203
204 Resources resources = context.getResources();
205 mBitmapCache = new BitmapCache(
206 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
207 mBitmapHolderCache = new BitmapHolderCache(mBitmapCache,
208 resources.getInteger(R.integer.config_photo_cache_max_bytes));
209 }
210
211 @Override
212 public void preloadPhotosInBackground() {
213 ensureLoaderThread();
214 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800215 }
216
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800217 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800218 public void loadPhoto(ImageView view, long photoId) {
219 if (photoId == 0) {
220 // No photo is needed
221 view.setImageResource(mDefaultResourceId);
222 mPendingRequests.remove(view);
223 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700224 loadPhotoByIdOrUri(view, photoId);
225 }
226 }
227
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800228 @Override
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700229 public void loadPhoto(ImageView view, Uri photoUri) {
230 if (photoUri == null) {
231 // No photo is needed
232 view.setImageResource(mDefaultResourceId);
233 mPendingRequests.remove(view);
234 } else {
235 loadPhotoByIdOrUri(view, photoUri);
236 }
237 }
238
239 private void loadPhotoByIdOrUri(ImageView view, Object key) {
240 boolean loaded = loadCachedPhoto(view, key);
241 if (loaded) {
242 mPendingRequests.remove(view);
243 } else {
244 mPendingRequests.put(view, key);
245 if (!mPaused) {
246 // Send a request to start loading photos
247 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800248 }
249 }
250 }
251
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800252 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800253 public void refreshCache() {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800254 for (BitmapHolder holder : mBitmapHolderCache.values()) {
255 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800256 }
257 }
258
259 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800260 * Checks if the photo is present in cache. If so, sets the photo on the view.
261 *
262 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800263 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700264 private boolean loadCachedPhoto(ImageView view, Object key) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800265 BitmapHolder holder = mBitmapHolderCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800266 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800267 // The bitmap has not been loaded - should display the placeholder image.
268 view.setImageResource(mDefaultResourceId);
269 return false;
270 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800271
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800272 if (holder.bytes == null) {
273 view.setImageResource(mDefaultResourceId);
274 return holder.fresh;
275 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800276
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800277 // Optionally decode bytes into a bitmap
278 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800279
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800280 view.setImageBitmap(holder.bitmap);
281
282 // Put the bitmap in the LRU cache
283 mBitmapCache.put(key, holder.bitmap);
284
285 // Soften the reference
286 holder.bitmap = null;
287
288 return holder.fresh;
289 }
290
291 /**
292 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
293 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
294 * the holder, it will not be necessary to decode the bitmap.
295 */
296 private void inflateBitmap(BitmapHolder holder) {
297 byte[] bytes = holder.bytes;
298 if (bytes == null || bytes.length == 0) {
299 return;
300 }
301
302 // Check the soft reference. If will be retained if the bitmap is also
303 // in the LRU cache, so we don't need to check the LRU cache explicitly.
304 if (holder.bitmapRef != null) {
305 holder.bitmap = holder.bitmapRef.get();
306 if (holder.bitmap != null) {
307 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800308 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800309 }
310
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800311 try {
312 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
313 holder.bitmap = bitmap;
314 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
315 } catch (OutOfMemoryError e) {
316 // Do nothing - the photo will appear to be missing
317 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800318 }
319
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700320 public void clear() {
321 mPendingRequests.clear();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800322 mBitmapHolderCache.clear();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700323 }
324
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800325 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800326 public void pause() {
327 mPaused = true;
328 }
329
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800330 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800331 public void resume() {
332 mPaused = false;
333 if (!mPendingRequests.isEmpty()) {
334 requestLoading();
335 }
336 }
337
338 /**
339 * Sends a message to this thread itself to start loading images. If the current
340 * view contains multiple image views, all of those image views will get a chance
341 * to request their respective photos before any of those requests are executed.
342 * This allows us to load images in bulk.
343 */
344 private void requestLoading() {
345 if (!mLoadingRequested) {
346 mLoadingRequested = true;
347 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
348 }
349 }
350
351 /**
352 * Processes requests on the main thread.
353 */
354 public boolean handleMessage(Message msg) {
355 switch (msg.what) {
356 case MESSAGE_REQUEST_LOADING: {
357 mLoadingRequested = false;
358 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800359 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800360 mLoaderThread.requestLoading();
361 }
362 return true;
363 }
364
365 case MESSAGE_PHOTOS_LOADED: {
366 if (!mPaused) {
367 processLoadedImages();
368 }
369 return true;
370 }
371 }
372 return false;
373 }
374
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800375 public void ensureLoaderThread() {
376 if (mLoaderThread == null) {
377 mLoaderThread = new LoaderThread(mContext.getContentResolver());
378 mLoaderThread.start();
379 }
380 }
381
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800382 /**
383 * Goes over pending loading requests and displays loaded photos. If some of the
384 * photos still haven't been loaded, sends another request for image loading.
385 */
386 private void processLoadedImages() {
387 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
388 while (iterator.hasNext()) {
389 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700390 Object key = mPendingRequests.get(view);
391 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800392 if (loaded) {
393 iterator.remove();
394 }
395 }
396
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800397 softenCache();
398
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800399 if (!mPendingRequests.isEmpty()) {
400 requestLoading();
401 }
402 }
403
404 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800405 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800406 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800407 */
408 private void softenCache() {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800409 for (BitmapHolder holder : mBitmapHolderCache.values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800410 holder.bitmap = null;
411 }
412 }
413
414 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800415 * Stores the supplied bitmap in cache.
416 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800417 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
418 BitmapHolder holder = new BitmapHolder(bytes);
419 holder.fresh = true;
420
421 // Unless this image is being preloaded, decode it right away while
422 // we are still on the background thread.
423 if (!preloading) {
424 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800425 }
426
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800427 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800428 }
429
430 /**
431 * Populates an array of photo IDs that need to be loaded.
432 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700433 private void obtainPhotoIdsAndUrisToLoad(ArrayList<Long> photoIds,
434 ArrayList<String> photoIdsAsStrings, ArrayList<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800435 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800436 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700437 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800438
439 /*
440 * Since the call is made from the loader thread, the map could be
441 * changing during the iteration. That's not really a problem:
442 * ConcurrentHashMap will allow those changes to happen without throwing
443 * exceptions. Since we may miss some requests in the situation of
444 * concurrent change, we will need to check the map again once loading
445 * is complete.
446 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700447 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800448 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700449 Object key = iterator.next();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800450 BitmapHolder holder = mBitmapHolderCache.get(key);
451 if (holder == null || !holder.fresh) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700452 if (key instanceof Long) {
453 photoIds.add((Long)key);
454 photoIdsAsStrings.add(key.toString());
455 } else {
456 uris.add((Uri)key);
457 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800458 }
459 }
460 }
461
462 /**
463 * The thread that performs loading of photos from the database.
464 */
465 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700466 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800467 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
468 private static final int MESSAGE_LOAD_PHOTOS = 1;
469
470 /**
471 * A pause between preload batches that yields to the UI thread.
472 */
473 private static final int PHOTO_PRELOAD_DELAY = 50;
474
475 /**
476 * Number of photos to preload per batch.
477 */
478 private static final int PRELOAD_BATCH = 25;
479
480 /**
481 * Maximum number of photos to preload. If the cache size is 2Mb and
482 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
483 */
484 private static final int MAX_PHOTOS_TO_PRELOAD = 500;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700485
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800486 private final ContentResolver mResolver;
487 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800488 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
489 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700490 private final ArrayList<Uri> mPhotoUris = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800491 private ArrayList<Long> mPreloadPhotoIds = Lists.newArrayList();
492
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800493 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700494 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800495
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800496 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
497 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
498 private static final int PRELOAD_STATUS_DONE = 2;
499
500 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
501
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800502 public LoaderThread(ContentResolver resolver) {
503 super(LOADER_THREAD_NAME);
504 mResolver = resolver;
505 }
506
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800507 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800508 if (mLoaderThreadHandler == null) {
509 mLoaderThreadHandler = new Handler(getLooper(), this);
510 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800511 }
512
513 /**
514 * Kicks off preloading of the next batch of photos on the background thread.
515 * Preloading will happen after a delay: we want to yield to the UI thread
516 * as much as possible.
517 * <p>
518 * If preloading is already complete, does nothing.
519 */
520 public void requestPreloading() {
521 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
522 return;
523 }
524
525 ensureHandler();
526 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
527 return;
528 }
529
530 mLoaderThreadHandler.sendEmptyMessageDelayed(
531 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
532 }
533
534 /**
535 * Sends a message to this thread to load requested photos. Cancels a preloading
536 * request, if any: we don't want preloading to impede loading of the photos
537 * we need to display now.
538 */
539 public void requestLoading() {
540 ensureHandler();
541 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
542 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800543 }
544
545 /**
546 * Receives the above message, loads photos and then sends a message
547 * to the main thread to process them.
548 */
549 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800550 switch (msg.what) {
551 case MESSAGE_PRELOAD_PHOTOS:
552 preloadPhotosInBackground();
553 break;
554 case MESSAGE_LOAD_PHOTOS:
555 loadPhotosInBackground();
556 break;
557 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800558 return true;
559 }
560
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800561 /**
562 * The first time it is called, figures out which photos need to be preloaded.
563 * Each subsequent call preloads the next batch of photos and requests
564 * another cycle of preloading after a delay. The whole process ends when
565 * we either run out of photos to preload or fill up cache.
566 */
567 private void preloadPhotosInBackground() {
568 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
569 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800570 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800571
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800572 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
573 queryPhotosForPreload();
574 if (mPreloadPhotoIds.isEmpty()) {
575 mPreloadStatus = PRELOAD_STATUS_DONE;
576 } else {
577 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
578 }
579 requestPreloading();
580 return;
581 }
582
583 if (mBitmapHolderCache.isFull()) {
584 mPreloadStatus = PRELOAD_STATUS_DONE;
585 return;
586 }
587
588 mPhotoIds.clear();
589 mPhotoIdsAsStrings.clear();
590
591 int count = 0;
592 int preloadSize = mPreloadPhotoIds.size();
593 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
594 preloadSize--;
595 count++;
596 Long photoId = mPreloadPhotoIds.get(preloadSize);
597 mPhotoIds.add(photoId);
598 mPhotoIdsAsStrings.add(photoId.toString());
599 mPreloadPhotoIds.remove(preloadSize);
600 }
601
602 loadPhotosFromDatabase(false);
603
604 if (preloadSize == 0) {
605 mPreloadStatus = PRELOAD_STATUS_DONE;
606 }
607
608 Log.v(TAG, "Preloaded " + count + " photos. Photos in cache: "
609 + mBitmapHolderCache.size()
610 + ". Total size: " + mBitmapHolderCache.getEstimatedSize());
611
612 requestPreloading();
613 }
614
615 private void queryPhotosForPreload() {
616 Cursor cursor = null;
617 try {
618 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
619 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
620 .build();
621 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
622 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
623 null,
624 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"
625 + " LIMIT " + MAX_PHOTOS_TO_PRELOAD);
626
627 if (cursor != null) {
628 while (cursor.moveToNext()) {
629 // Insert them in reverse order, because we will be taking
630 // them from the end of the list for loading.
631 mPreloadPhotoIds.add(0, cursor.getLong(0));
632 }
633 }
634 } finally {
635 if (cursor != null) {
636 cursor.close();
637 }
638 }
639 }
640
641 private void loadPhotosInBackground() {
642 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
643 loadPhotosFromDatabase(true);
644 loadRemotePhotos();
645 requestPreloading();
646 }
647
648 private void loadPhotosFromDatabase(boolean preloading) {
649 int count = mPhotoIds.size();
650 if (count == 0) {
651 return;
652 }
653
654 // Remove loaded photos from the preload queue: we don't want
655 // the preloading process to load them again.
656 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
657 for (int i = 0; i < count; i++) {
658 mPreloadPhotoIds.remove(mPhotoIds.get(i));
659 }
660 if (mPreloadPhotoIds.isEmpty()) {
661 mPreloadStatus = PRELOAD_STATUS_DONE;
662 }
663 }
664
665 mStringBuilder.setLength(0);
666 mStringBuilder.append(Photo._ID + " IN(");
667 for (int i = 0; i < count; i++) {
668 if (i != 0) {
669 mStringBuilder.append(',');
670 }
671 mStringBuilder.append('?');
672 }
673 mStringBuilder.append(')');
674
675 Cursor cursor = null;
676 try {
677 cursor = mResolver.query(Data.CONTENT_URI,
678 COLUMNS,
679 mStringBuilder.toString(),
680 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
681 null);
682
683 if (cursor != null) {
684 while (cursor.moveToNext()) {
685 Long id = cursor.getLong(0);
686 byte[] bytes = cursor.getBlob(1);
687 cacheBitmap(id, bytes, preloading);
688 mPhotoIds.remove(id);
689 }
690 }
691 } finally {
692 if (cursor != null) {
693 cursor.close();
694 }
695 }
696
697 // Remaining photos were not found in the database - mark the cache accordingly.
698 count = mPhotoIds.size();
699 for (int i = 0; i < count; i++) {
700 cacheBitmap(mPhotoIds.get(i), null, preloading);
701 }
702
703 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
704 }
705
706 private void loadRemotePhotos() {
707 int count = mPhotoUris.size();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800708 for (int i = 0; i < count; i++) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700709 Uri uri = mPhotoUris.get(i);
710 if (mBuffer == null) {
711 mBuffer = new byte[BUFFER_SIZE];
712 }
713 try {
714 InputStream is = mResolver.openInputStream(uri);
715 if (is != null) {
716 ByteArrayOutputStream baos = new ByteArrayOutputStream();
717 try {
718 int size;
719 while ((size = is.read(mBuffer)) != -1) {
720 baos.write(mBuffer, 0, size);
721 }
722 } finally {
723 is.close();
724 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800725 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700726 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700727 } else {
728 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800729 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700730 }
731 } catch (Exception ex) {
732 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800733 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700734 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800735 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800736 }
737 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800738
739 /**
740 * An LRU cache of {@link BitmapHolder}'s. It estimates the total size of
741 * loaded bitmaps and caps that number.
742 */
743 private static class BitmapHolderCache extends LinkedHashMap<Object, BitmapHolder> {
744 private final BitmapCache mBitmapCache;
745 private final int mMaxBytes;
746 private final int mRedZoneBytes;
747 private int mEstimatedBytes;
748
749 public BitmapHolderCache(BitmapCache bitmapCache, int maxBytes) {
750 super(BITMAP_CACHE_INITIAL_CAPACITY, 0.75f, true);
751 this.mBitmapCache = bitmapCache;
752 mMaxBytes = maxBytes;
753 mRedZoneBytes = (int) (mMaxBytes * 0.75);
754 }
755
756 // Leave unsynchronized: if the result is a bit off, that's ok
757 public boolean isFull() {
758 return mEstimatedBytes > mRedZoneBytes;
759 }
760
761 public int getEstimatedSize() {
762 return mEstimatedBytes;
763 }
764
765 @Override
766 public synchronized BitmapHolder get(Object key) {
767 return super.get(key);
768 }
769
770 @Override
771 public synchronized BitmapHolder put(Object key, BitmapHolder newValue) {
772 BitmapHolder oldValue = get(key);
773 if (oldValue != null && oldValue.bytes != null) {
774 mEstimatedBytes -= oldValue.bytes.length;
775 }
776 if (newValue.bytes != null) {
777 mEstimatedBytes += newValue.bytes.length;
778 }
779 return super.put(key, newValue);
780 }
781
782 @Override
783 public BitmapHolder remove(Object key) {
784 BitmapHolder value = get(key);
785 if (value != null && value.bytes != null) {
786 mEstimatedBytes -= value.bytes.length;
787 }
788 mBitmapCache.remove(key);
789 return super.remove(key);
790 }
791
792 @Override
793 protected boolean removeEldestEntry(Map.Entry<Object, BitmapHolder> eldest) {
794 return mEstimatedBytes > mMaxBytes;
795 }
796 }
797
798 /**
799 * An LRU cache of bitmaps. These are the most recently used bitmaps that we want
800 * to protect from GC. The rest of bitmaps are softly retained and will be
801 * gradually released by GC.
802 */
803 private static class BitmapCache extends LinkedHashMap<Object, Bitmap> {
804 private int mMaxEntries;
805
806 public BitmapCache(int maxEntries) {
807 super(BITMAP_CACHE_INITIAL_CAPACITY, 0.75f, true);
808 mMaxEntries = maxEntries;
809 }
810
811 @Override
812 protected boolean removeEldestEntry(Map.Entry<Object, Bitmap> eldest) {
813 return size() > mMaxEntries;
814 }
815 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800816}