blob: b2ceffadb3e7ded33b7253f589f4cc1ee49e3472 [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;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070031import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080032import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070033import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080034import android.os.HandlerThread;
35import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080036import android.provider.ContactsContract;
37import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080038import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070039import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080040import android.provider.ContactsContract.Directory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070041import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080042import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080043import android.widget.ImageView;
44
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070045import java.io.ByteArrayOutputStream;
46import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070047import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080048import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080049import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010050import java.util.List;
51import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080052import java.util.concurrent.ConcurrentHashMap;
53
54/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080055 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080056 */
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080057public abstract class ContactPhotoManager {
58
59 static final String TAG = "ContactPhotoManager";
60
61 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
62
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070063 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
64 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
65 if (hires) return R.drawable.ic_contact_picture_180_holo_light;
66 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
67 return R.drawable.ic_contact_picture_holo_light;
68 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080069
70 /**
71 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
72 * the available authenticators. This method can safely be called from the UI thread.
73 */
74 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010075 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080076 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +010077 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080078 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +010079 service = createContactPhotoManager(applicationContext);
80 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080081 }
82 return service;
83 }
84
85 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
86 return new ContactPhotoManagerImpl(context);
87 }
88
89 /**
90 * Load photo into the supplied image view. If the photo is already cached,
91 * it is displayed immediately. Otherwise a request is sent to load the photo
92 * from the database.
93 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070094 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080095
96 /**
97 * Load photo into the supplied image view. If the photo is already cached,
98 * it is displayed immediately. Otherwise a request is sent to load the photo
99 * from the location specified by the URI.
100 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700101 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800102
103 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700104 * Remove photo from the supplied image view. This also cancels current pending load request
105 * inside this photo manager.
106 */
107 public abstract void removePhoto(ImageView view);
108
109 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800110 * Temporarily stops loading photos from the database.
111 */
112 public abstract void pause();
113
114 /**
115 * Resumes loading photos from the database.
116 */
117 public abstract void resume();
118
119 /**
120 * Marks all cached photos for reloading. We can continue using cache but should
121 * also make sure the photos haven't changed in the background and notify the views
122 * if so.
123 */
124 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800125
126 /**
127 * Initiates a background process that over time will fill up cache with
128 * preload photos.
129 */
130 public abstract void preloadPhotosInBackground();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800131}
132
133class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800134 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
135
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800136 /**
137 * Type of message sent by the UI thread to itself to indicate that some photos
138 * need to be loaded.
139 */
140 private static final int MESSAGE_REQUEST_LOADING = 1;
141
142 /**
143 * Type of message sent by the loader thread to indicate that some photos have
144 * been loaded.
145 */
146 private static final int MESSAGE_PHOTOS_LOADED = 2;
147
148 private static final String[] EMPTY_STRING_ARRAY = new String[0];
149
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800150 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800151
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800152 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800153 * Maintains the state of a particular photo.
154 */
155 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800156 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800157
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800158 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800159 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700160 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800161
162 public BitmapHolder(byte[] bytes) {
163 this.bytes = bytes;
164 this.fresh = true;
165 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800166 }
167
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800168 private final Context mContext;
169
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800170 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800171 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800172 * as they come from the database. Each holder has a soft reference to the
173 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800174 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800175 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
176
177 /**
178 * Cache size threshold at which bitmaps will not be preloaded.
179 */
180 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800181
182 /**
183 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
184 * the most recently used bitmaps to save time on decoding
185 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
186 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800187 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800188
189 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700190 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
191 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800192 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700193 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
194 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800195
196 /**
197 * Handler for messages sent to the UI thread.
198 */
199 private final Handler mMainThreadHandler = new Handler(this);
200
201 /**
202 * Thread responsible for loading photos from the database. Created upon
203 * the first request.
204 */
205 private LoaderThread mLoaderThread;
206
207 /**
208 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
209 */
210 private boolean mLoadingRequested;
211
212 /**
213 * Flag indicating if the image loading is paused.
214 */
215 private boolean mPaused;
216
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800217 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800218 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800219
220 Resources resources = context.getResources();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800221 mBitmapCache = new LruCache<Object, Bitmap>(
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800222 resources.getInteger(R.integer.config_photo_cache_max_bitmaps));
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800223 int maxBytes = resources.getInteger(R.integer.config_photo_cache_max_bytes);
224 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(maxBytes) {
225 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800226 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800227 }
228 };
229 mBitmapHolderCacheRedZoneBytes = (int) (maxBytes * 0.75);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800230 }
231
232 @Override
233 public void preloadPhotosInBackground() {
234 ensureLoaderThread();
235 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800236 }
237
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800238 @Override
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700239 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800240 if (photoId == 0) {
241 // No photo is needed
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700242 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800243 mPendingRequests.remove(view);
244 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700245 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700246 }
247 }
248
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800249 @Override
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700250 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700251 if (photoUri == null) {
252 // No photo is needed
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700253 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700254 mPendingRequests.remove(view);
255 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700256 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700257 }
258 }
259
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700260 private void loadPhotoByIdOrUri(ImageView view, Request request) {
261 boolean loaded = loadCachedPhoto(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700262 if (loaded) {
263 mPendingRequests.remove(view);
264 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700265 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700266 if (!mPaused) {
267 // Send a request to start loading photos
268 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800269 }
270 }
271 }
272
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800273 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700274 public void removePhoto(ImageView view) {
275 view.setImageDrawable(null);
276 mPendingRequests.remove(view);
277 }
278
279 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800280 public void refreshCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800281 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800282 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800283 }
284 }
285
286 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800287 * Checks if the photo is present in cache. If so, sets the photo on the view.
288 *
289 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800290 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700291 private boolean loadCachedPhoto(ImageView view, Request request) {
292 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800293 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800294 // The bitmap has not been loaded - should display the placeholder image.
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700295 view.setImageResource(getDefaultAvatarResId(request.isHires(), request.isDarkTheme()));
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800296 return false;
297 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800298
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800299 if (holder.bytes == null) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700300 view.setImageResource(getDefaultAvatarResId(request.isHires(), request.isDarkTheme()));
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800301 return holder.fresh;
302 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800303
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800304 // Optionally decode bytes into a bitmap
305 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800306
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800307 view.setImageBitmap(holder.bitmap);
308
309 // Put the bitmap in the LRU cache
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700310 mBitmapCache.put(request, holder.bitmap);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800311
312 // Soften the reference
313 holder.bitmap = null;
314
315 return holder.fresh;
316 }
317
318 /**
319 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
320 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
321 * the holder, it will not be necessary to decode the bitmap.
322 */
323 private void inflateBitmap(BitmapHolder holder) {
324 byte[] bytes = holder.bytes;
325 if (bytes == null || bytes.length == 0) {
326 return;
327 }
328
329 // Check the soft reference. If will be retained if the bitmap is also
330 // in the LRU cache, so we don't need to check the LRU cache explicitly.
331 if (holder.bitmapRef != null) {
332 holder.bitmap = holder.bitmapRef.get();
333 if (holder.bitmap != null) {
334 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800335 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800336 }
337
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800338 try {
339 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
340 holder.bitmap = bitmap;
341 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
342 } catch (OutOfMemoryError e) {
343 // Do nothing - the photo will appear to be missing
344 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800345 }
346
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700347 public void clear() {
348 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800349 mBitmapHolderCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700350 }
351
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800352 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800353 public void pause() {
354 mPaused = true;
355 }
356
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800357 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800358 public void resume() {
359 mPaused = false;
360 if (!mPendingRequests.isEmpty()) {
361 requestLoading();
362 }
363 }
364
365 /**
366 * Sends a message to this thread itself to start loading images. If the current
367 * view contains multiple image views, all of those image views will get a chance
368 * to request their respective photos before any of those requests are executed.
369 * This allows us to load images in bulk.
370 */
371 private void requestLoading() {
372 if (!mLoadingRequested) {
373 mLoadingRequested = true;
374 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
375 }
376 }
377
378 /**
379 * Processes requests on the main thread.
380 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700381 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800382 public boolean handleMessage(Message msg) {
383 switch (msg.what) {
384 case MESSAGE_REQUEST_LOADING: {
385 mLoadingRequested = false;
386 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800387 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800388 mLoaderThread.requestLoading();
389 }
390 return true;
391 }
392
393 case MESSAGE_PHOTOS_LOADED: {
394 if (!mPaused) {
395 processLoadedImages();
396 }
397 return true;
398 }
399 }
400 return false;
401 }
402
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800403 public void ensureLoaderThread() {
404 if (mLoaderThread == null) {
405 mLoaderThread = new LoaderThread(mContext.getContentResolver());
406 mLoaderThread.start();
407 }
408 }
409
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800410 /**
411 * Goes over pending loading requests and displays loaded photos. If some of the
412 * photos still haven't been loaded, sends another request for image loading.
413 */
414 private void processLoadedImages() {
415 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
416 while (iterator.hasNext()) {
417 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700418 Request key = mPendingRequests.get(view);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700419 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800420 if (loaded) {
421 iterator.remove();
422 }
423 }
424
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800425 softenCache();
426
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800427 if (!mPendingRequests.isEmpty()) {
428 requestLoading();
429 }
430 }
431
432 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800433 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800434 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800435 */
436 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800437 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800438 holder.bitmap = null;
439 }
440 }
441
442 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800443 * Stores the supplied bitmap in cache.
444 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800445 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
446 BitmapHolder holder = new BitmapHolder(bytes);
447 holder.fresh = true;
448
449 // Unless this image is being preloaded, decode it right away while
450 // we are still on the background thread.
451 if (!preloading) {
452 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800453 }
454
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800455 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800456 }
457
458 /**
459 * Populates an array of photo IDs that need to be loaded.
460 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100461 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
462 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800463 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800464 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700465 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800466
467 /*
468 * Since the call is made from the loader thread, the map could be
469 * changing during the iteration. That's not really a problem:
470 * ConcurrentHashMap will allow those changes to happen without throwing
471 * exceptions. Since we may miss some requests in the situation of
472 * concurrent change, we will need to check the map again once loading
473 * is complete.
474 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700475 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800476 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700477 Request request = iterator.next();
478 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800479 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700480 if (request.isUriRequest()) {
481 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700482 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700483 photoIds.add(request.mId);
484 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700485 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800486 }
487 }
488 }
489
490 /**
491 * The thread that performs loading of photos from the database.
492 */
493 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700494 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800495 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
496 private static final int MESSAGE_LOAD_PHOTOS = 1;
497
498 /**
499 * A pause between preload batches that yields to the UI thread.
500 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700501 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800502
503 /**
504 * Number of photos to preload per batch.
505 */
506 private static final int PRELOAD_BATCH = 25;
507
508 /**
509 * Maximum number of photos to preload. If the cache size is 2Mb and
510 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
511 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700512 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700513
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800514 private final ContentResolver mResolver;
515 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100516 private final Set<Long> mPhotoIds = Sets.newHashSet();
517 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
518 private final Set<Uri> mPhotoUris = Sets.newHashSet();
519 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800520
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800521 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700522 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800523
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800524 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
525 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
526 private static final int PRELOAD_STATUS_DONE = 2;
527
528 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
529
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800530 public LoaderThread(ContentResolver resolver) {
531 super(LOADER_THREAD_NAME);
532 mResolver = resolver;
533 }
534
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800535 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800536 if (mLoaderThreadHandler == null) {
537 mLoaderThreadHandler = new Handler(getLooper(), this);
538 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800539 }
540
541 /**
542 * Kicks off preloading of the next batch of photos on the background thread.
543 * Preloading will happen after a delay: we want to yield to the UI thread
544 * as much as possible.
545 * <p>
546 * If preloading is already complete, does nothing.
547 */
548 public void requestPreloading() {
549 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
550 return;
551 }
552
553 ensureHandler();
554 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
555 return;
556 }
557
558 mLoaderThreadHandler.sendEmptyMessageDelayed(
559 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
560 }
561
562 /**
563 * Sends a message to this thread to load requested photos. Cancels a preloading
564 * request, if any: we don't want preloading to impede loading of the photos
565 * we need to display now.
566 */
567 public void requestLoading() {
568 ensureHandler();
569 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
570 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800571 }
572
573 /**
574 * Receives the above message, loads photos and then sends a message
575 * to the main thread to process them.
576 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700577 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800578 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800579 switch (msg.what) {
580 case MESSAGE_PRELOAD_PHOTOS:
581 preloadPhotosInBackground();
582 break;
583 case MESSAGE_LOAD_PHOTOS:
584 loadPhotosInBackground();
585 break;
586 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800587 return true;
588 }
589
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800590 /**
591 * The first time it is called, figures out which photos need to be preloaded.
592 * Each subsequent call preloads the next batch of photos and requests
593 * another cycle of preloading after a delay. The whole process ends when
594 * we either run out of photos to preload or fill up cache.
595 */
596 private void preloadPhotosInBackground() {
597 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
598 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800599 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800600
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800601 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
602 queryPhotosForPreload();
603 if (mPreloadPhotoIds.isEmpty()) {
604 mPreloadStatus = PRELOAD_STATUS_DONE;
605 } else {
606 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
607 }
608 requestPreloading();
609 return;
610 }
611
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800612 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800613 mPreloadStatus = PRELOAD_STATUS_DONE;
614 return;
615 }
616
617 mPhotoIds.clear();
618 mPhotoIdsAsStrings.clear();
619
620 int count = 0;
621 int preloadSize = mPreloadPhotoIds.size();
622 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
623 preloadSize--;
624 count++;
625 Long photoId = mPreloadPhotoIds.get(preloadSize);
626 mPhotoIds.add(photoId);
627 mPhotoIdsAsStrings.add(photoId.toString());
628 mPreloadPhotoIds.remove(preloadSize);
629 }
630
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700631 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800632
633 if (preloadSize == 0) {
634 mPreloadStatus = PRELOAD_STATUS_DONE;
635 }
636
Makoto Onuki173f2812011-09-06 14:49:27 -0700637 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
638 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800639
640 requestPreloading();
641 }
642
643 private void queryPhotosForPreload() {
644 Cursor cursor = null;
645 try {
646 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
647 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700648 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
649 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800650 .build();
651 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
652 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
653 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700654 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800655
656 if (cursor != null) {
657 while (cursor.moveToNext()) {
658 // Insert them in reverse order, because we will be taking
659 // them from the end of the list for loading.
660 mPreloadPhotoIds.add(0, cursor.getLong(0));
661 }
662 }
663 } finally {
664 if (cursor != null) {
665 cursor.close();
666 }
667 }
668 }
669
670 private void loadPhotosInBackground() {
671 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700672 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800673 loadRemotePhotos();
674 requestPreloading();
675 }
676
677 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100678 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800679 return;
680 }
681
682 // Remove loaded photos from the preload queue: we don't want
683 // the preloading process to load them again.
684 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100685 for (Long id : mPhotoIds) {
686 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800687 }
688 if (mPreloadPhotoIds.isEmpty()) {
689 mPreloadStatus = PRELOAD_STATUS_DONE;
690 }
691 }
692
693 mStringBuilder.setLength(0);
694 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100695 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800696 if (i != 0) {
697 mStringBuilder.append(',');
698 }
699 mStringBuilder.append('?');
700 }
701 mStringBuilder.append(')');
702
703 Cursor cursor = null;
704 try {
Dave Santoro84cac442011-08-24 15:23:10 -0700705 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800706 COLUMNS,
707 mStringBuilder.toString(),
708 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
709 null);
710
711 if (cursor != null) {
712 while (cursor.moveToNext()) {
713 Long id = cursor.getLong(0);
714 byte[] bytes = cursor.getBlob(1);
715 cacheBitmap(id, bytes, preloading);
716 mPhotoIds.remove(id);
717 }
718 }
719 } finally {
720 if (cursor != null) {
721 cursor.close();
722 }
723 }
724
Dave Santoro84cac442011-08-24 15:23:10 -0700725 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100726 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700727 if (ContactsContract.isProfileId(id)) {
728 Cursor profileCursor = null;
729 try {
730 profileCursor = mResolver.query(
731 ContentUris.withAppendedId(Data.CONTENT_URI, id),
732 COLUMNS, null, null, null);
733 if (profileCursor != null && profileCursor.moveToFirst()) {
734 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
735 preloading);
736 } else {
737 // Couldn't load a photo this way either.
738 cacheBitmap(id, null, preloading);
739 }
740 } finally {
741 if (profileCursor != null) {
742 profileCursor.close();
743 }
744 }
745 } else {
746 // Not a profile photo and not found - mark the cache accordingly
747 cacheBitmap(id, null, preloading);
748 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800749 }
750
751 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
752 }
753
754 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100755 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700756 if (mBuffer == null) {
757 mBuffer = new byte[BUFFER_SIZE];
758 }
759 try {
760 InputStream is = mResolver.openInputStream(uri);
761 if (is != null) {
762 ByteArrayOutputStream baos = new ByteArrayOutputStream();
763 try {
764 int size;
765 while ((size = is.read(mBuffer)) != -1) {
766 baos.write(mBuffer, 0, size);
767 }
768 } finally {
769 is.close();
770 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800771 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700772 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700773 } else {
774 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800775 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700776 }
777 } catch (Exception ex) {
778 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800779 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700780 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800781 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800782 }
783 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700784
785 /**
786 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
787 * light theme
788 */
789 private static final class Request {
790 private final long mId;
791 private final Uri mUri;
792 private final boolean mDarkTheme;
793 private final boolean mHires;
794
795 private Request(long id, Uri uri, boolean hires, boolean darkTheme) {
796 mId = id;
797 mUri = uri;
798 mDarkTheme = darkTheme;
799 mHires = hires;
800 }
801
802 public static Request createFromId(long id, boolean hires, boolean darkTheme) {
803 return new Request(id, null /* no URI */, hires, darkTheme);
804 }
805
806 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme) {
807 return new Request(0 /* no ID */, uri, hires, darkTheme);
808 }
809
810 public boolean isDarkTheme() {
811 return mDarkTheme;
812 }
813
814 public boolean isHires() {
815 return mHires;
816 }
817
818 public boolean isUriRequest() {
819 return mUri != null;
820 }
821
822 @Override
823 public int hashCode() {
824 if (mUri != null) return mUri.hashCode();
825
826 // copied over from Long.hashCode()
827 return (int) (mId ^ (mId >>> 32));
828 }
829
830 @Override
831 public boolean equals(Object o) {
832 if (!(o instanceof Request)) return false;
833 final Request that = (Request) o;
834 // Don't compare equality of mHires and mDarkTheme fields because these are only used
835 // in the default contact photo case. When the contact does have a photo, the contact
836 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
837 // the photo request on the queue twice.
838 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
839 }
840
841 public Object getKey() {
842 return mUri == null ? mId : mUri;
843 }
844 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800845}