blob: 7b54666a43d20d3b31841e88f4964d2e9ce73b65 [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
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700362 if (holder.bitmap != null) {
363 // Put the bitmap in the LRU cache
364 mBitmapCache.put(request, holder.bitmap);
365 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800366
367 // Soften the reference
368 holder.bitmap = null;
369
370 return holder.fresh;
371 }
372
373 /**
374 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
375 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
376 * the holder, it will not be necessary to decode the bitmap.
377 */
378 private void inflateBitmap(BitmapHolder holder) {
379 byte[] bytes = holder.bytes;
380 if (bytes == null || bytes.length == 0) {
381 return;
382 }
383
384 // Check the soft reference. If will be retained if the bitmap is also
385 // in the LRU cache, so we don't need to check the LRU cache explicitly.
386 if (holder.bitmapRef != null) {
387 holder.bitmap = holder.bitmapRef.get();
388 if (holder.bitmap != null) {
389 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800390 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800391 }
392
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800393 try {
394 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
395 holder.bitmap = bitmap;
396 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
397 } catch (OutOfMemoryError e) {
398 // Do nothing - the photo will appear to be missing
399 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800400 }
401
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700402 public void clear() {
403 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800404 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700405 }
406
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800407 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800408 public void pause() {
409 mPaused = true;
410 }
411
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800412 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800413 public void resume() {
414 mPaused = false;
415 if (!mPendingRequests.isEmpty()) {
416 requestLoading();
417 }
418 }
419
420 /**
421 * Sends a message to this thread itself to start loading images. If the current
422 * view contains multiple image views, all of those image views will get a chance
423 * to request their respective photos before any of those requests are executed.
424 * This allows us to load images in bulk.
425 */
426 private void requestLoading() {
427 if (!mLoadingRequested) {
428 mLoadingRequested = true;
429 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
430 }
431 }
432
433 /**
434 * Processes requests on the main thread.
435 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700436 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800437 public boolean handleMessage(Message msg) {
438 switch (msg.what) {
439 case MESSAGE_REQUEST_LOADING: {
440 mLoadingRequested = false;
441 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800442 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800443 mLoaderThread.requestLoading();
444 }
445 return true;
446 }
447
448 case MESSAGE_PHOTOS_LOADED: {
449 if (!mPaused) {
450 processLoadedImages();
451 }
452 return true;
453 }
454 }
455 return false;
456 }
457
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800458 public void ensureLoaderThread() {
459 if (mLoaderThread == null) {
460 mLoaderThread = new LoaderThread(mContext.getContentResolver());
461 mLoaderThread.start();
462 }
463 }
464
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800465 /**
466 * Goes over pending loading requests and displays loaded photos. If some of the
467 * photos still haven't been loaded, sends another request for image loading.
468 */
469 private void processLoadedImages() {
470 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
471 while (iterator.hasNext()) {
472 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700473 Request key = mPendingRequests.get(view);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700474 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800475 if (loaded) {
476 iterator.remove();
477 }
478 }
479
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800480 softenCache();
481
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800482 if (!mPendingRequests.isEmpty()) {
483 requestLoading();
484 }
485 }
486
487 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800488 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800489 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800490 */
491 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800492 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800493 holder.bitmap = null;
494 }
495 }
496
497 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800498 * Stores the supplied bitmap in cache.
499 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800500 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
501 BitmapHolder holder = new BitmapHolder(bytes);
502 holder.fresh = true;
503
504 // Unless this image is being preloaded, decode it right away while
505 // we are still on the background thread.
506 if (!preloading) {
507 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800508 }
509
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800510 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800511 }
512
513 /**
514 * Populates an array of photo IDs that need to be loaded.
515 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100516 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
517 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800518 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800519 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700520 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800521
522 /*
523 * Since the call is made from the loader thread, the map could be
524 * changing during the iteration. That's not really a problem:
525 * ConcurrentHashMap will allow those changes to happen without throwing
526 * exceptions. Since we may miss some requests in the situation of
527 * concurrent change, we will need to check the map again once loading
528 * is complete.
529 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700530 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800531 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700532 Request request = iterator.next();
533 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800534 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700535 if (request.isUriRequest()) {
536 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700537 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700538 photoIds.add(request.mId);
539 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700540 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800541 }
542 }
543 }
544
545 /**
546 * The thread that performs loading of photos from the database.
547 */
548 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700549 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800550 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
551 private static final int MESSAGE_LOAD_PHOTOS = 1;
552
553 /**
554 * A pause between preload batches that yields to the UI thread.
555 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700556 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800557
558 /**
559 * Number of photos to preload per batch.
560 */
561 private static final int PRELOAD_BATCH = 25;
562
563 /**
564 * Maximum number of photos to preload. If the cache size is 2Mb and
565 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
566 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700567 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700568
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800569 private final ContentResolver mResolver;
570 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100571 private final Set<Long> mPhotoIds = Sets.newHashSet();
572 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
573 private final Set<Uri> mPhotoUris = Sets.newHashSet();
574 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800575
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800576 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700577 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800578
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800579 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
580 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
581 private static final int PRELOAD_STATUS_DONE = 2;
582
583 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
584
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800585 public LoaderThread(ContentResolver resolver) {
586 super(LOADER_THREAD_NAME);
587 mResolver = resolver;
588 }
589
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800590 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800591 if (mLoaderThreadHandler == null) {
592 mLoaderThreadHandler = new Handler(getLooper(), this);
593 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800594 }
595
596 /**
597 * Kicks off preloading of the next batch of photos on the background thread.
598 * Preloading will happen after a delay: we want to yield to the UI thread
599 * as much as possible.
600 * <p>
601 * If preloading is already complete, does nothing.
602 */
603 public void requestPreloading() {
604 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
605 return;
606 }
607
608 ensureHandler();
609 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
610 return;
611 }
612
613 mLoaderThreadHandler.sendEmptyMessageDelayed(
614 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
615 }
616
617 /**
618 * Sends a message to this thread to load requested photos. Cancels a preloading
619 * request, if any: we don't want preloading to impede loading of the photos
620 * we need to display now.
621 */
622 public void requestLoading() {
623 ensureHandler();
624 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
625 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800626 }
627
628 /**
629 * Receives the above message, loads photos and then sends a message
630 * to the main thread to process them.
631 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700632 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800633 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800634 switch (msg.what) {
635 case MESSAGE_PRELOAD_PHOTOS:
636 preloadPhotosInBackground();
637 break;
638 case MESSAGE_LOAD_PHOTOS:
639 loadPhotosInBackground();
640 break;
641 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800642 return true;
643 }
644
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800645 /**
646 * The first time it is called, figures out which photos need to be preloaded.
647 * Each subsequent call preloads the next batch of photos and requests
648 * another cycle of preloading after a delay. The whole process ends when
649 * we either run out of photos to preload or fill up cache.
650 */
651 private void preloadPhotosInBackground() {
652 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
653 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800654 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800655
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800656 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
657 queryPhotosForPreload();
658 if (mPreloadPhotoIds.isEmpty()) {
659 mPreloadStatus = PRELOAD_STATUS_DONE;
660 } else {
661 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
662 }
663 requestPreloading();
664 return;
665 }
666
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800667 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800668 mPreloadStatus = PRELOAD_STATUS_DONE;
669 return;
670 }
671
672 mPhotoIds.clear();
673 mPhotoIdsAsStrings.clear();
674
675 int count = 0;
676 int preloadSize = mPreloadPhotoIds.size();
677 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
678 preloadSize--;
679 count++;
680 Long photoId = mPreloadPhotoIds.get(preloadSize);
681 mPhotoIds.add(photoId);
682 mPhotoIdsAsStrings.add(photoId.toString());
683 mPreloadPhotoIds.remove(preloadSize);
684 }
685
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700686 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800687
688 if (preloadSize == 0) {
689 mPreloadStatus = PRELOAD_STATUS_DONE;
690 }
691
Makoto Onuki173f2812011-09-06 14:49:27 -0700692 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
693 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800694
695 requestPreloading();
696 }
697
698 private void queryPhotosForPreload() {
699 Cursor cursor = null;
700 try {
701 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
702 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700703 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
704 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800705 .build();
706 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
707 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
708 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700709 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800710
711 if (cursor != null) {
712 while (cursor.moveToNext()) {
713 // Insert them in reverse order, because we will be taking
714 // them from the end of the list for loading.
715 mPreloadPhotoIds.add(0, cursor.getLong(0));
716 }
717 }
718 } finally {
719 if (cursor != null) {
720 cursor.close();
721 }
722 }
723 }
724
725 private void loadPhotosInBackground() {
726 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700727 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800728 loadRemotePhotos();
729 requestPreloading();
730 }
731
732 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100733 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800734 return;
735 }
736
737 // Remove loaded photos from the preload queue: we don't want
738 // the preloading process to load them again.
739 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100740 for (Long id : mPhotoIds) {
741 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800742 }
743 if (mPreloadPhotoIds.isEmpty()) {
744 mPreloadStatus = PRELOAD_STATUS_DONE;
745 }
746 }
747
748 mStringBuilder.setLength(0);
749 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100750 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800751 if (i != 0) {
752 mStringBuilder.append(',');
753 }
754 mStringBuilder.append('?');
755 }
756 mStringBuilder.append(')');
757
758 Cursor cursor = null;
759 try {
Dave Santoro84cac442011-08-24 15:23:10 -0700760 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800761 COLUMNS,
762 mStringBuilder.toString(),
763 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
764 null);
765
766 if (cursor != null) {
767 while (cursor.moveToNext()) {
768 Long id = cursor.getLong(0);
769 byte[] bytes = cursor.getBlob(1);
770 cacheBitmap(id, bytes, preloading);
771 mPhotoIds.remove(id);
772 }
773 }
774 } finally {
775 if (cursor != null) {
776 cursor.close();
777 }
778 }
779
Dave Santoro84cac442011-08-24 15:23:10 -0700780 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100781 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700782 if (ContactsContract.isProfileId(id)) {
783 Cursor profileCursor = null;
784 try {
785 profileCursor = mResolver.query(
786 ContentUris.withAppendedId(Data.CONTENT_URI, id),
787 COLUMNS, null, null, null);
788 if (profileCursor != null && profileCursor.moveToFirst()) {
789 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
790 preloading);
791 } else {
792 // Couldn't load a photo this way either.
793 cacheBitmap(id, null, preloading);
794 }
795 } finally {
796 if (profileCursor != null) {
797 profileCursor.close();
798 }
799 }
800 } else {
801 // Not a profile photo and not found - mark the cache accordingly
802 cacheBitmap(id, null, preloading);
803 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800804 }
805
806 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
807 }
808
809 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100810 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700811 if (mBuffer == null) {
812 mBuffer = new byte[BUFFER_SIZE];
813 }
814 try {
815 InputStream is = mResolver.openInputStream(uri);
816 if (is != null) {
817 ByteArrayOutputStream baos = new ByteArrayOutputStream();
818 try {
819 int size;
820 while ((size = is.read(mBuffer)) != -1) {
821 baos.write(mBuffer, 0, size);
822 }
823 } finally {
824 is.close();
825 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800826 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700827 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700828 } else {
829 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800830 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700831 }
832 } catch (Exception ex) {
833 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800834 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700835 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800836 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800837 }
838 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700839
840 /**
841 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
842 * light theme
843 */
844 private static final class Request {
845 private final long mId;
846 private final Uri mUri;
847 private final boolean mDarkTheme;
848 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700849 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700850
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700851 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
852 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700853 mId = id;
854 mUri = uri;
855 mDarkTheme = darkTheme;
856 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700857 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700858 }
859
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700860 public static Request createFromId(long id, boolean hires, boolean darkTheme,
861 DefaultImageProvider defaultProvider) {
862 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700863 }
864
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700865 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
866 DefaultImageProvider defaultProvider) {
867 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700868 }
869
870 public boolean isDarkTheme() {
871 return mDarkTheme;
872 }
873
874 public boolean isHires() {
875 return mHires;
876 }
877
878 public boolean isUriRequest() {
879 return mUri != null;
880 }
881
882 @Override
883 public int hashCode() {
884 if (mUri != null) return mUri.hashCode();
885
886 // copied over from Long.hashCode()
887 return (int) (mId ^ (mId >>> 32));
888 }
889
890 @Override
891 public boolean equals(Object o) {
892 if (!(o instanceof Request)) return false;
893 final Request that = (Request) o;
894 // Don't compare equality of mHires and mDarkTheme fields because these are only used
895 // in the default contact photo case. When the contact does have a photo, the contact
896 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
897 // the photo request on the queue twice.
898 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
899 }
900
901 public Object getKey() {
902 return mUri == null ? mId : mUri;
903 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700904
905 public void applyDefaultImage(ImageView view) {
906 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
907 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700908 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800909}