blob: e04a07a3a937aa64b567481d8dbde6e709f6447d [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;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070020import com.android.contacts.util.UriUtils;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080021import com.google.android.collect.Lists;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010022import com.google.android.collect.Sets;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080023
24import android.content.ContentResolver;
Dave Santoro84cac442011-08-24 15:23:10 -070025import android.content.ContentUris;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080026import android.content.Context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080027import android.content.res.Resources;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080028import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070031import android.graphics.drawable.ColorDrawable;
32import android.graphics.drawable.Drawable;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070033import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080034import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070035import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080036import android.os.HandlerThread;
37import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080038import android.provider.ContactsContract;
39import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080040import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070041import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080042import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070043import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080044import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080045import android.widget.ImageView;
46
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070047import java.io.ByteArrayOutputStream;
48import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070049import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080050import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080051import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010052import java.util.List;
53import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080054import java.util.concurrent.ConcurrentHashMap;
55
56/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080057 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080058 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080059public abstract class ContactPhotoManager {
60
61 static final String TAG = "ContactPhotoManager";
62
63 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
64
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070065 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
66 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
67 if (hires) return R.drawable.ic_contact_picture_180_holo_light;
68 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
69 return R.drawable.ic_contact_picture_holo_light;
70 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080071
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070072 public static abstract class DefaultImageProvider {
73 public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
74 }
75
76 private static class AvatarDefaultImageProvider extends DefaultImageProvider {
77 @Override
78 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
79 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
80 }
81 }
82
83 private static class BlankDefaultImageProvider extends DefaultImageProvider {
84 private static Drawable sDrawable;
85
86 @Override
87 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
88 if (sDrawable == null) {
89 Context context = view.getContext();
90 sDrawable = new ColorDrawable(context.getResources().getColor(
91 R.color.image_placeholder));
92 }
93 view.setImageDrawable(sDrawable);
94 }
95 }
96
97 public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider();
98
99 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
100
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800101 /**
102 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
103 * the available authenticators. This method can safely be called from the UI thread.
104 */
105 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100106 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800107 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +0100108 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800109 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100110 service = createContactPhotoManager(applicationContext);
111 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800112 }
113 return service;
114 }
115
116 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
117 return new ContactPhotoManagerImpl(context);
118 }
119
120 /**
121 * Load photo into the supplied image view. If the photo is already cached,
122 * it is displayed immediately. Otherwise a request is sent to load the photo
123 * from the database.
124 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700125 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
126 DefaultImageProvider defaultProvider);
127
128 /**
129 * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
130 * {@link #DEFAULT_AVATER}.
131 */
132 public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
133 loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER);
134 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800135
136 /**
137 * Load photo into the supplied image view. If the photo is already cached,
138 * it is displayed immediately. Otherwise a request is sent to load the photo
139 * from the location specified by the URI.
140 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700141 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
142 DefaultImageProvider defaultProvider);
143
144 /**
145 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
146 * {@link #DEFAULT_AVATER}.
147 */
148 public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
149 loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER);
150 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800151
152 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700153 * Remove photo from the supplied image view. This also cancels current pending load request
154 * inside this photo manager.
155 */
156 public abstract void removePhoto(ImageView view);
157
158 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800159 * Temporarily stops loading photos from the database.
160 */
161 public abstract void pause();
162
163 /**
164 * Resumes loading photos from the database.
165 */
166 public abstract void resume();
167
168 /**
169 * Marks all cached photos for reloading. We can continue using cache but should
170 * also make sure the photos haven't changed in the background and notify the views
171 * if so.
172 */
173 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800174
175 /**
176 * Initiates a background process that over time will fill up cache with
177 * preload photos.
178 */
179 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800180}
181
182class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800183 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
184
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800185 /**
186 * Type of message sent by the UI thread to itself to indicate that some photos
187 * need to be loaded.
188 */
189 private static final int MESSAGE_REQUEST_LOADING = 1;
190
191 /**
192 * Type of message sent by the loader thread to indicate that some photos have
193 * been loaded.
194 */
195 private static final int MESSAGE_PHOTOS_LOADED = 2;
196
197 private static final String[] EMPTY_STRING_ARRAY = new String[0];
198
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800199 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800200
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800201 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800202 * Maintains the state of a particular photo.
203 */
204 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800205 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800206
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800207 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800208 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700209 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800210
211 public BitmapHolder(byte[] bytes) {
212 this.bytes = bytes;
213 this.fresh = true;
214 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800215 }
216
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800217 private final Context mContext;
218
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800219 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800220 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800221 * as they come from the database. Each holder has a soft reference to the
222 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800223 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800224 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
225
226 /**
227 * Cache size threshold at which bitmaps will not be preloaded.
228 */
229 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800230
231 /**
232 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
233 * the most recently used bitmaps to save time on decoding
234 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
235 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800236 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800237
238 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700239 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
240 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800241 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700242 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
243 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800244
245 /**
246 * Handler for messages sent to the UI thread.
247 */
248 private final Handler mMainThreadHandler = new Handler(this);
249
250 /**
251 * Thread responsible for loading photos from the database. Created upon
252 * the first request.
253 */
254 private LoaderThread mLoaderThread;
255
256 /**
257 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
258 */
259 private boolean mLoadingRequested;
260
261 /**
262 * Flag indicating if the image loading is paused.
263 */
264 private boolean mPaused;
265
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800266 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800267 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800268
269 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800270 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800271 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800272 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
273 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
274 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800275 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800276 }
277 };
278 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800279 }
280
281 @Override
282 public void preloadPhotosInBackground() {
283 ensureLoaderThread();
284 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800285 }
286
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800287 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700288 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
289 DefaultImageProvider defaultProvider) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800290 if (photoId == 0) {
291 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700292 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800293 mPendingRequests.remove(view);
294 } else {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700295 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
296 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700297 }
298 }
299
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800300 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700301 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
302 DefaultImageProvider defaultProvider) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700303 if (photoUri == null) {
304 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700305 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700306 mPendingRequests.remove(view);
307 } else {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700308 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
309 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700310 }
311 }
312
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700313 private void loadPhotoByIdOrUri(ImageView view, Request request) {
314 boolean loaded = loadCachedPhoto(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700315 if (loaded) {
316 mPendingRequests.remove(view);
317 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700318 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700319 if (!mPaused) {
320 // Send a request to start loading photos
321 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800322 }
323 }
324 }
325
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800326 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700327 public void removePhoto(ImageView view) {
328 view.setImageDrawable(null);
329 mPendingRequests.remove(view);
330 }
331
332 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800333 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800334 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800335 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800336 }
337 }
338
339 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800340 * Checks if the photo is present in cache. If so, sets the photo on the view.
341 *
342 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800343 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700344 private boolean loadCachedPhoto(ImageView view, Request request) {
345 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800346 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800347 // The bitmap has not been loaded - should display the placeholder image.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700348 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800349 return false;
350 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800351
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800352 if (holder.bytes == null) {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700353 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800354 return holder.fresh;
355 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800356
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800357 // Optionally decode bytes into a bitmap
358 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800359
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800360 view.setImageBitmap(holder.bitmap);
361
362 // Put the bitmap in the LRU cache
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700363 mBitmapCache.put(request, holder.bitmap);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800364
365 // Soften the reference
366 holder.bitmap = null;
367
368 return holder.fresh;
369 }
370
371 /**
372 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
373 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
374 * the holder, it will not be necessary to decode the bitmap.
375 */
376 private void inflateBitmap(BitmapHolder holder) {
377 byte[] bytes = holder.bytes;
378 if (bytes == null || bytes.length == 0) {
379 return;
380 }
381
382 // Check the soft reference. If will be retained if the bitmap is also
383 // in the LRU cache, so we don't need to check the LRU cache explicitly.
384 if (holder.bitmapRef != null) {
385 holder.bitmap = holder.bitmapRef.get();
386 if (holder.bitmap != null) {
387 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800388 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800389 }
390
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800391 try {
392 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
393 holder.bitmap = bitmap;
394 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
395 } catch (OutOfMemoryError e) {
396 // Do nothing - the photo will appear to be missing
397 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800398 }
399
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700400 public void clear() {
401 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800402 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700403 }
404
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800405 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800406 public void pause() {
407 mPaused = true;
408 }
409
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800410 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800411 public void resume() {
412 mPaused = false;
413 if (!mPendingRequests.isEmpty()) {
414 requestLoading();
415 }
416 }
417
418 /**
419 * Sends a message to this thread itself to start loading images. If the current
420 * view contains multiple image views, all of those image views will get a chance
421 * to request their respective photos before any of those requests are executed.
422 * This allows us to load images in bulk.
423 */
424 private void requestLoading() {
425 if (!mLoadingRequested) {
426 mLoadingRequested = true;
427 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
428 }
429 }
430
431 /**
432 * Processes requests on the main thread.
433 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700434 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800435 public boolean handleMessage(Message msg) {
436 switch (msg.what) {
437 case MESSAGE_REQUEST_LOADING: {
438 mLoadingRequested = false;
439 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800440 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800441 mLoaderThread.requestLoading();
442 }
443 return true;
444 }
445
446 case MESSAGE_PHOTOS_LOADED: {
447 if (!mPaused) {
448 processLoadedImages();
449 }
450 return true;
451 }
452 }
453 return false;
454 }
455
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800456 public void ensureLoaderThread() {
457 if (mLoaderThread == null) {
458 mLoaderThread = new LoaderThread(mContext.getContentResolver());
459 mLoaderThread.start();
460 }
461 }
462
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800463 /**
464 * Goes over pending loading requests and displays loaded photos. If some of the
465 * photos still haven't been loaded, sends another request for image loading.
466 */
467 private void processLoadedImages() {
468 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
469 while (iterator.hasNext()) {
470 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700471 Request key = mPendingRequests.get(view);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700472 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800473 if (loaded) {
474 iterator.remove();
475 }
476 }
477
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800478 softenCache();
479
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800480 if (!mPendingRequests.isEmpty()) {
481 requestLoading();
482 }
483 }
484
485 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800486 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800487 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800488 */
489 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800490 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800491 holder.bitmap = null;
492 }
493 }
494
495 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800496 * Stores the supplied bitmap in cache.
497 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800498 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
499 BitmapHolder holder = new BitmapHolder(bytes);
500 holder.fresh = true;
501
502 // Unless this image is being preloaded, decode it right away while
503 // we are still on the background thread.
504 if (!preloading) {
505 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800506 }
507
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800508 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800509 }
510
511 /**
512 * Populates an array of photo IDs that need to be loaded.
513 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100514 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
515 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800516 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800517 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700518 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800519
520 /*
521 * Since the call is made from the loader thread, the map could be
522 * changing during the iteration. That's not really a problem:
523 * ConcurrentHashMap will allow those changes to happen without throwing
524 * exceptions. Since we may miss some requests in the situation of
525 * concurrent change, we will need to check the map again once loading
526 * is complete.
527 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700528 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800529 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700530 Request request = iterator.next();
531 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800532 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700533 if (request.isUriRequest()) {
534 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700535 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700536 photoIds.add(request.mId);
537 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700538 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800539 }
540 }
541 }
542
543 /**
544 * The thread that performs loading of photos from the database.
545 */
546 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700547 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800548 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
549 private static final int MESSAGE_LOAD_PHOTOS = 1;
550
551 /**
552 * A pause between preload batches that yields to the UI thread.
553 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700554 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800555
556 /**
557 * Number of photos to preload per batch.
558 */
559 private static final int PRELOAD_BATCH = 25;
560
561 /**
562 * Maximum number of photos to preload. If the cache size is 2Mb and
563 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
564 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700565 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700566
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800567 private final ContentResolver mResolver;
568 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100569 private final Set<Long> mPhotoIds = Sets.newHashSet();
570 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
571 private final Set<Uri> mPhotoUris = Sets.newHashSet();
572 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800573
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800574 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700575 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800576
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800577 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
578 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
579 private static final int PRELOAD_STATUS_DONE = 2;
580
581 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
582
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800583 public LoaderThread(ContentResolver resolver) {
584 super(LOADER_THREAD_NAME);
585 mResolver = resolver;
586 }
587
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800588 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800589 if (mLoaderThreadHandler == null) {
590 mLoaderThreadHandler = new Handler(getLooper(), this);
591 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800592 }
593
594 /**
595 * Kicks off preloading of the next batch of photos on the background thread.
596 * Preloading will happen after a delay: we want to yield to the UI thread
597 * as much as possible.
598 * <p>
599 * If preloading is already complete, does nothing.
600 */
601 public void requestPreloading() {
602 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
603 return;
604 }
605
606 ensureHandler();
607 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
608 return;
609 }
610
611 mLoaderThreadHandler.sendEmptyMessageDelayed(
612 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
613 }
614
615 /**
616 * Sends a message to this thread to load requested photos. Cancels a preloading
617 * request, if any: we don't want preloading to impede loading of the photos
618 * we need to display now.
619 */
620 public void requestLoading() {
621 ensureHandler();
622 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
623 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800624 }
625
626 /**
627 * Receives the above message, loads photos and then sends a message
628 * to the main thread to process them.
629 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700630 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800631 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800632 switch (msg.what) {
633 case MESSAGE_PRELOAD_PHOTOS:
634 preloadPhotosInBackground();
635 break;
636 case MESSAGE_LOAD_PHOTOS:
637 loadPhotosInBackground();
638 break;
639 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800640 return true;
641 }
642
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800643 /**
644 * The first time it is called, figures out which photos need to be preloaded.
645 * Each subsequent call preloads the next batch of photos and requests
646 * another cycle of preloading after a delay. The whole process ends when
647 * we either run out of photos to preload or fill up cache.
648 */
649 private void preloadPhotosInBackground() {
650 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
651 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800652 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800653
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800654 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
655 queryPhotosForPreload();
656 if (mPreloadPhotoIds.isEmpty()) {
657 mPreloadStatus = PRELOAD_STATUS_DONE;
658 } else {
659 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
660 }
661 requestPreloading();
662 return;
663 }
664
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800665 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800666 mPreloadStatus = PRELOAD_STATUS_DONE;
667 return;
668 }
669
670 mPhotoIds.clear();
671 mPhotoIdsAsStrings.clear();
672
673 int count = 0;
674 int preloadSize = mPreloadPhotoIds.size();
675 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
676 preloadSize--;
677 count++;
678 Long photoId = mPreloadPhotoIds.get(preloadSize);
679 mPhotoIds.add(photoId);
680 mPhotoIdsAsStrings.add(photoId.toString());
681 mPreloadPhotoIds.remove(preloadSize);
682 }
683
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700684 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800685
686 if (preloadSize == 0) {
687 mPreloadStatus = PRELOAD_STATUS_DONE;
688 }
689
Makoto Onuki173f2812011-09-06 14:49:27 -0700690 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
691 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800692
693 requestPreloading();
694 }
695
696 private void queryPhotosForPreload() {
697 Cursor cursor = null;
698 try {
699 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
700 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700701 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
702 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800703 .build();
704 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
705 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
706 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700707 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800708
709 if (cursor != null) {
710 while (cursor.moveToNext()) {
711 // Insert them in reverse order, because we will be taking
712 // them from the end of the list for loading.
713 mPreloadPhotoIds.add(0, cursor.getLong(0));
714 }
715 }
716 } finally {
717 if (cursor != null) {
718 cursor.close();
719 }
720 }
721 }
722
723 private void loadPhotosInBackground() {
724 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700725 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800726 loadRemotePhotos();
727 requestPreloading();
728 }
729
730 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100731 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800732 return;
733 }
734
735 // Remove loaded photos from the preload queue: we don't want
736 // the preloading process to load them again.
737 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100738 for (Long id : mPhotoIds) {
739 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800740 }
741 if (mPreloadPhotoIds.isEmpty()) {
742 mPreloadStatus = PRELOAD_STATUS_DONE;
743 }
744 }
745
746 mStringBuilder.setLength(0);
747 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100748 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800749 if (i != 0) {
750 mStringBuilder.append(',');
751 }
752 mStringBuilder.append('?');
753 }
754 mStringBuilder.append(')');
755
756 Cursor cursor = null;
757 try {
Dave Santoro84cac442011-08-24 15:23:10 -0700758 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800759 COLUMNS,
760 mStringBuilder.toString(),
761 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
762 null);
763
764 if (cursor != null) {
765 while (cursor.moveToNext()) {
766 Long id = cursor.getLong(0);
767 byte[] bytes = cursor.getBlob(1);
768 cacheBitmap(id, bytes, preloading);
769 mPhotoIds.remove(id);
770 }
771 }
772 } finally {
773 if (cursor != null) {
774 cursor.close();
775 }
776 }
777
Dave Santoro84cac442011-08-24 15:23:10 -0700778 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100779 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700780 if (ContactsContract.isProfileId(id)) {
781 Cursor profileCursor = null;
782 try {
783 profileCursor = mResolver.query(
784 ContentUris.withAppendedId(Data.CONTENT_URI, id),
785 COLUMNS, null, null, null);
786 if (profileCursor != null && profileCursor.moveToFirst()) {
787 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
788 preloading);
789 } else {
790 // Couldn't load a photo this way either.
791 cacheBitmap(id, null, preloading);
792 }
793 } finally {
794 if (profileCursor != null) {
795 profileCursor.close();
796 }
797 }
798 } else {
799 // Not a profile photo and not found - mark the cache accordingly
800 cacheBitmap(id, null, preloading);
801 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800802 }
803
804 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
805 }
806
807 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100808 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700809 if (mBuffer == null) {
810 mBuffer = new byte[BUFFER_SIZE];
811 }
812 try {
813 InputStream is = mResolver.openInputStream(uri);
814 if (is != null) {
815 ByteArrayOutputStream baos = new ByteArrayOutputStream();
816 try {
817 int size;
818 while ((size = is.read(mBuffer)) != -1) {
819 baos.write(mBuffer, 0, size);
820 }
821 } finally {
822 is.close();
823 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800824 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700825 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700826 } else {
827 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800828 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700829 }
830 } catch (Exception ex) {
831 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800832 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700833 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800834 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800835 }
836 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700837
838 /**
839 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
840 * light theme
841 */
842 private static final class Request {
843 private final long mId;
844 private final Uri mUri;
845 private final boolean mDarkTheme;
846 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700847 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700848
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700849 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
850 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700851 mId = id;
852 mUri = uri;
853 mDarkTheme = darkTheme;
854 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700855 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700856 }
857
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700858 public static Request createFromId(long id, boolean hires, boolean darkTheme,
859 DefaultImageProvider defaultProvider) {
860 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700861 }
862
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700863 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
864 DefaultImageProvider defaultProvider) {
865 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700866 }
867
868 public boolean isDarkTheme() {
869 return mDarkTheme;
870 }
871
872 public boolean isHires() {
873 return mHires;
874 }
875
876 public boolean isUriRequest() {
877 return mUri != null;
878 }
879
880 @Override
881 public int hashCode() {
882 if (mUri != null) return mUri.hashCode();
883
884 // copied over from Long.hashCode()
885 return (int) (mId ^ (mId >>> 32));
886 }
887
888 @Override
889 public boolean equals(Object o) {
890 if (!(o instanceof Request)) return false;
891 final Request that = (Request) o;
892 // Don't compare equality of mHires and mDarkTheme fields because these are only used
893 // in the default contact photo case. When the contact does have a photo, the contact
894 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
895 // the photo request on the queue twice.
896 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
897 }
898
899 public Object getKey() {
900 return mUri == null ? mId : mUri;
901 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700902
903 public void applyDefaultImage(ImageView view) {
904 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
905 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700906 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800907}