blob: 3002dc3aae4868491f06ff04803912c54cec0990 [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;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080020import com.android.contacts.util.MemoryUtils;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070021import com.android.contacts.util.UriUtils;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080022import com.google.android.collect.Lists;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010023import com.google.android.collect.Sets;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080024
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080025import android.content.ComponentCallbacks2;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080026import android.content.ContentResolver;
Dave Santoro84cac442011-08-24 15:23:10 -070027import android.content.ContentUris;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080028import android.content.Context;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080029import android.content.res.Configuration;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080030import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.graphics.BitmapFactory;
Daniel Lehmann7a46cde2012-02-01 17:53:44 -080033import android.graphics.drawable.BitmapDrawable;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070034import android.graphics.drawable.ColorDrawable;
35import android.graphics.drawable.Drawable;
Daniel Lehmann7a46cde2012-02-01 17:53:44 -080036import android.graphics.drawable.TransitionDrawable;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070037import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080038import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070039import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080040import android.os.HandlerThread;
41import android.os.Message;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080042import android.provider.ContactsContract;
43import android.provider.ContactsContract.Contacts;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080044import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070045import android.provider.ContactsContract.Data;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -080046import android.provider.ContactsContract.Directory;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080047import android.text.TextUtils;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070048import android.util.Log;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -080049import android.util.LruCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080050import android.widget.ImageView;
51
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070052import java.io.ByteArrayOutputStream;
53import java.io.InputStream;
Makoto Onuki173f2812011-09-06 14:49:27 -070054import java.lang.ref.Reference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080055import java.lang.ref.SoftReference;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080056import java.util.Iterator;
Flavio Lerdad33b18c2011-07-17 22:03:15 +010057import java.util.List;
58import java.util.Set;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080059import java.util.concurrent.ConcurrentHashMap;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080060import java.util.concurrent.atomic.AtomicInteger;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080061
62/**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080063 * Asynchronously loads contact photos and maintains a cache of photos.
Dmitri Plotnikove8643852010-02-17 10:49:05 -080064 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080065public abstract class ContactPhotoManager implements ComponentCallbacks2 {
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080066 static final String TAG = "ContactPhotoManager";
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -080067 static final boolean DEBUG = false; // Don't submit with true
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080068
69 public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
70
Daniel Lehmannecfc26c2011-09-12 17:44:35 -070071 public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
72 if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
73 if (hires) return R.drawable.ic_contact_picture_180_holo_light;
74 if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
75 return R.drawable.ic_contact_picture_holo_light;
76 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -080077
Makoto Onuki3d3a15c2011-09-22 10:55:08 -070078 public static abstract class DefaultImageProvider {
79 public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
80 }
81
82 private static class AvatarDefaultImageProvider extends DefaultImageProvider {
83 @Override
84 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
85 view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
86 }
87 }
88
89 private static class BlankDefaultImageProvider extends DefaultImageProvider {
90 private static Drawable sDrawable;
91
92 @Override
93 public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
94 if (sDrawable == null) {
95 Context context = view.getContext();
96 sDrawable = new ColorDrawable(context.getResources().getColor(
97 R.color.image_placeholder));
98 }
99 view.setImageDrawable(sDrawable);
100 }
101 }
102
103 public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider();
104
105 public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
106
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800107 /**
108 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
109 * the available authenticators. This method can safely be called from the UI thread.
110 */
111 public static ContactPhotoManager getInstance(Context context) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100112 Context applicationContext = context.getApplicationContext();
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800113 ContactPhotoManager service =
Flavio Lerda82e4a562011-07-08 17:05:31 +0100114 (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800115 if (service == null) {
Flavio Lerda82e4a562011-07-08 17:05:31 +0100116 service = createContactPhotoManager(applicationContext);
117 Log.e(TAG, "No contact photo service in context: " + applicationContext);
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800118 }
119 return service;
120 }
121
122 public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
123 return new ContactPhotoManagerImpl(context);
124 }
125
126 /**
127 * Load photo into the supplied image view. If the photo is already cached,
128 * it is displayed immediately. Otherwise a request is sent to load the photo
129 * from the database.
130 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700131 public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
132 DefaultImageProvider defaultProvider);
133
134 /**
135 * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
136 * {@link #DEFAULT_AVATER}.
137 */
138 public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
139 loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER);
140 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800141
142 /**
143 * Load photo into the supplied image view. If the photo is already cached,
144 * it is displayed immediately. Otherwise a request is sent to load the photo
145 * from the location specified by the URI.
146 */
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700147 public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
148 DefaultImageProvider defaultProvider);
149
150 /**
151 * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
152 * {@link #DEFAULT_AVATER}.
153 */
154 public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
155 loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER);
156 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800157
158 /**
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700159 * Remove photo from the supplied image view. This also cancels current pending load request
160 * inside this photo manager.
161 */
162 public abstract void removePhoto(ImageView view);
163
164 /**
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800165 * Temporarily stops loading photos from the database.
166 */
167 public abstract void pause();
168
169 /**
170 * Resumes loading photos from the database.
171 */
172 public abstract void resume();
173
174 /**
175 * Marks all cached photos for reloading. We can continue using cache but should
176 * also make sure the photos haven't changed in the background and notify the views
177 * if so.
178 */
179 public abstract void refreshCache();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800180
181 /**
Dave Santoro14d20832011-12-02 18:14:52 -0800182 * Stores the given bitmap directly in the LRU bitmap cache.
183 * @param photoUri The URI of the photo (for future requests).
184 * @param bitmap The bitmap.
185 * @param photoBytes The bytes that were parsed to create the bitmap.
186 */
187 public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
188
189 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800190 * Initiates a background process that over time will fill up cache with
191 * preload photos.
192 */
193 public abstract void preloadPhotosInBackground();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800194
195 // ComponentCallbacks2
196 @Override
197 public void onConfigurationChanged(Configuration newConfig) {
198 }
199
200 // ComponentCallbacks2
201 @Override
202 public void onLowMemory() {
203 }
204
205 // ComponentCallbacks2
206 @Override
207 public void onTrimMemory(int level) {
208 }
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800209}
210
211class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800212 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
213
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800214 private static final int FADE_TRANSITION_DURATION = 200;
215
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800216 /**
217 * Type of message sent by the UI thread to itself to indicate that some photos
218 * need to be loaded.
219 */
220 private static final int MESSAGE_REQUEST_LOADING = 1;
221
222 /**
223 * Type of message sent by the loader thread to indicate that some photos have
224 * been loaded.
225 */
226 private static final int MESSAGE_PHOTOS_LOADED = 2;
227
228 private static final String[] EMPTY_STRING_ARRAY = new String[0];
229
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800230 private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800231
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800232 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800233 * Maintains the state of a particular photo.
234 */
235 private static class BitmapHolder {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800236 final byte[] bytes;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800237
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800238 volatile boolean fresh;
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800239 Bitmap bitmap;
Makoto Onuki173f2812011-09-06 14:49:27 -0700240 Reference<Bitmap> bitmapRef;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800241
242 public BitmapHolder(byte[] bytes) {
243 this.bytes = bytes;
244 this.fresh = true;
245 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800246 }
247
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800248 private final Context mContext;
249
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800250 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800251 * An LRU cache for bitmap holders. The cache contains bytes for photos just
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800252 * as they come from the database. Each holder has a soft reference to the
253 * actual bitmap.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800254 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800255 private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
256
257 /**
258 * Cache size threshold at which bitmaps will not be preloaded.
259 */
260 private final int mBitmapHolderCacheRedZoneBytes;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800261
262 /**
263 * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
264 * the most recently used bitmaps to save time on decoding
265 * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
266 */
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800267 private final LruCache<Object, Bitmap> mBitmapCache;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800268
269 /**
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700270 * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
271 * The request may swapped out before the photo loading request is started.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800272 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700273 private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
274 new ConcurrentHashMap<ImageView, Request>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800275
276 /**
277 * Handler for messages sent to the UI thread.
278 */
279 private final Handler mMainThreadHandler = new Handler(this);
280
281 /**
282 * Thread responsible for loading photos from the database. Created upon
283 * the first request.
284 */
285 private LoaderThread mLoaderThread;
286
287 /**
288 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
289 */
290 private boolean mLoadingRequested;
291
292 /**
293 * Flag indicating if the image loading is paused.
294 */
295 private boolean mPaused;
296
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800297 /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
298 private static final int HOLDER_CACHE_SIZE = 2000000;
299
300 /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
301 private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
302
303 private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
304
305 /** For debug: How many times we had to reload cached photo for a stale entry */
306 private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
307
308 /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
309 private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
310
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800311 public ContactPhotoManagerImpl(Context context) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800312 mContext = context;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800313
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800314 final float cacheSizeAdjustment =
315 (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
316 final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
317 mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
318 @Override protected int sizeOf(Object key, Bitmap value) {
319 return value.getByteCount();
320 }
321
322 @Override protected void entryRemoved(
323 boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
324 if (DEBUG) dumpStats();
325 }
326 };
327 final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
328 mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800329 @Override protected int sizeOf(Object key, BitmapHolder value) {
Jesse Wilsonab79a262011-02-09 15:34:31 -0800330 return value.bytes != null ? value.bytes.length : 0;
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800331 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800332
333 @Override protected void entryRemoved(
334 boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
335 if (DEBUG) dumpStats();
336 }
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800337 };
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800338 mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
339 Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
340 if (DEBUG) {
341 Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
342 + " + " + btk(mBitmapCache.maxSize()));
343 }
344 }
345
346 /** Converts bytes to K bytes, rounding up. Used only for debug log. */
347 private static String btk(int bytes) {
348 return ((bytes + 1023) / 1024) + "K";
349 }
350
351 private static final int safeDiv(int dividend, int divisor) {
352 return (divisor == 0) ? 0 : (dividend / divisor);
353 }
354
355 /**
356 * Dump cache stats on logcat.
357 */
358 private void dumpStats() {
359 if (!DEBUG) return;
360 {
361 int numHolders = 0;
362 int rawBytes = 0;
363 int bitmapBytes = 0;
364 int numBitmaps = 0;
365 for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
366 numHolders++;
367 if (h.bytes != null) {
368 rawBytes += h.bytes.length;
369 }
370 Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
371 if (b != null) {
372 numBitmaps++;
373 bitmapBytes += b.getByteCount();
374 }
375 }
376 Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
377 + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
378 + numBitmaps + " bitmaps, avg: "
379 + btk(safeDiv(rawBytes, numHolders))
380 + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
381 Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
382 + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
383 + " stale=" + mStaleCacheOverwrite.get());
384 }
385
386 {
387 int numBitmaps = 0;
388 int bitmapBytes = 0;
389 for (Bitmap b : mBitmapCache.snapshot().values()) {
390 numBitmaps++;
391 bitmapBytes += b.getByteCount();
392 }
393 Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
394 + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
395 // We don't get from L2 cache, so L2 stats is meaningless.
396 }
397 }
398
399 @Override
400 public void onTrimMemory(int level) {
401 if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
402 if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
403 // Clear the caches. Note all pending requests will be removed too.
404 clear();
405 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800406 }
407
408 @Override
409 public void preloadPhotosInBackground() {
410 ensureLoaderThread();
411 mLoaderThread.requestPreloading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800412 }
413
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800414 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700415 public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
416 DefaultImageProvider defaultProvider) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800417 if (photoId == 0) {
418 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700419 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800420 mPendingRequests.remove(view);
421 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800422 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700423 loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
424 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700425 }
426 }
427
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800428 @Override
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700429 public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
430 DefaultImageProvider defaultProvider) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700431 if (photoUri == null) {
432 // No photo is needed
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700433 defaultProvider.applyDefaultImage(view, hires, darkTheme);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700434 mPendingRequests.remove(view);
435 } else {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800436 if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700437 loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
438 defaultProvider));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700439 }
440 }
441
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700442 private void loadPhotoByIdOrUri(ImageView view, Request request) {
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800443 boolean loaded = loadCachedPhoto(view, request, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700444 if (loaded) {
445 mPendingRequests.remove(view);
446 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700447 mPendingRequests.put(view, request);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700448 if (!mPaused) {
449 // Send a request to start loading photos
450 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800451 }
452 }
453 }
454
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800455 @Override
Daisuke Miyakawaf5be9ba2011-08-05 09:17:07 -0700456 public void removePhoto(ImageView view) {
457 view.setImageDrawable(null);
458 mPendingRequests.remove(view);
459 }
460
461 @Override
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800462 public void refreshCache() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800463 if (DEBUG) Log.d(TAG, "refreshCache");
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800464 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800465 holder.fresh = false;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800466 }
467 }
468
469 /**
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800470 * Checks if the photo is present in cache. If so, sets the photo on the view.
471 *
472 * @return false if the photo needs to be (re)loaded from the provider.
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800473 */
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800474 private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
475 Bitmap bitmap = mBitmapCache.get(request.getKey());
476 if (bitmap != null) {
477 view.setImageBitmap(bitmap);
478 return true;
479 }
480
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700481 BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800482 if (holder == null) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800483 // The bitmap has not been loaded - should display the placeholder image.
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700484 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800485 return false;
486 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800487
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800488 if (holder.bytes == null) {
Makoto Onuki3d3a15c2011-09-22 10:55:08 -0700489 request.applyDefaultImage(view);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800490 return holder.fresh;
491 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800492
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800493 // Optionally decode bytes into a bitmap
494 inflateBitmap(holder);
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800495
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800496 if (fadeIn) {
497 Drawable[] layers = new Drawable[2];
498 layers[0] = mContext.getResources().getDrawable(
499 getDefaultAvatarResId(request.mHires, request.mDarkTheme));
500 layers[1] = new BitmapDrawable(mContext.getResources(), holder.bitmap);
501 TransitionDrawable drawable = new TransitionDrawable(layers);
502 view.setImageDrawable(drawable);
503 drawable.startTransition(FADE_TRANSITION_DURATION);
504 } else {
505 view.setImageBitmap(holder.bitmap);
506 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800507
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700508 if (holder.bitmap != null) {
509 // Put the bitmap in the LRU cache
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800510 mBitmapCache.put(request.getKey(), holder.bitmap);
Daisuke Miyakawabf9b2132011-10-11 19:03:52 -0700511 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800512
513 // Soften the reference
514 holder.bitmap = null;
515
516 return holder.fresh;
517 }
518
519 /**
520 * If necessary, decodes bytes stored in the holder to Bitmap. As long as the
521 * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
522 * the holder, it will not be necessary to decode the bitmap.
523 */
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800524 private static void inflateBitmap(BitmapHolder holder) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800525 byte[] bytes = holder.bytes;
526 if (bytes == null || bytes.length == 0) {
527 return;
528 }
529
530 // Check the soft reference. If will be retained if the bitmap is also
531 // in the LRU cache, so we don't need to check the LRU cache explicitly.
532 if (holder.bitmapRef != null) {
533 holder.bitmap = holder.bitmapRef.get();
534 if (holder.bitmap != null) {
535 return;
Dmitri Plotnikov718a2502010-11-23 17:56:28 -0800536 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800537 }
538
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800539 try {
540 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
541 holder.bitmap = bitmap;
542 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800543 if (DEBUG) {
544 Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
545 + bitmap.getWidth() + "x" + bitmap.getHeight()
546 + ", " + btk(bitmap.getByteCount()));
547 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800548 } catch (OutOfMemoryError e) {
549 // Do nothing - the photo will appear to be missing
550 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800551 }
552
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700553 public void clear() {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800554 if (DEBUG) Log.d(TAG, "clear");
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700555 mPendingRequests.clear();
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800556 mBitmapHolderCache.evictAll();
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800557 mBitmapCache.evictAll();
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700558 }
559
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800560 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800561 public void pause() {
562 mPaused = true;
563 }
564
Dmitri Plotnikov34b24ef2011-01-28 13:41:07 -0800565 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800566 public void resume() {
567 mPaused = false;
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800568 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800569 if (!mPendingRequests.isEmpty()) {
570 requestLoading();
571 }
572 }
573
574 /**
575 * Sends a message to this thread itself to start loading images. If the current
576 * view contains multiple image views, all of those image views will get a chance
577 * to request their respective photos before any of those requests are executed.
578 * This allows us to load images in bulk.
579 */
580 private void requestLoading() {
581 if (!mLoadingRequested) {
582 mLoadingRequested = true;
583 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
584 }
585 }
586
587 /**
588 * Processes requests on the main thread.
589 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700590 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800591 public boolean handleMessage(Message msg) {
592 switch (msg.what) {
593 case MESSAGE_REQUEST_LOADING: {
594 mLoadingRequested = false;
595 if (!mPaused) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800596 ensureLoaderThread();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800597 mLoaderThread.requestLoading();
598 }
599 return true;
600 }
601
602 case MESSAGE_PHOTOS_LOADED: {
603 if (!mPaused) {
604 processLoadedImages();
605 }
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800606 if (DEBUG) dumpStats();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800607 return true;
608 }
609 }
610 return false;
611 }
612
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800613 public void ensureLoaderThread() {
614 if (mLoaderThread == null) {
615 mLoaderThread = new LoaderThread(mContext.getContentResolver());
616 mLoaderThread.start();
617 }
618 }
619
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800620 /**
621 * Goes over pending loading requests and displays loaded photos. If some of the
622 * photos still haven't been loaded, sends another request for image loading.
623 */
624 private void processLoadedImages() {
625 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
626 while (iterator.hasNext()) {
627 ImageView view = iterator.next();
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700628 Request key = mPendingRequests.get(view);
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800629 boolean loaded = loadCachedPhoto(view, key, true);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800630 if (loaded) {
631 iterator.remove();
632 }
633 }
634
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800635 softenCache();
636
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800637 if (!mPendingRequests.isEmpty()) {
638 requestLoading();
639 }
640 }
641
642 /**
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800643 * Removes strong references to loaded bitmaps to allow them to be garbage collected
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800644 * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800645 */
646 private void softenCache() {
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800647 for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
Dmitri Plotnikov37dc7cf2010-12-17 19:25:08 -0800648 holder.bitmap = null;
649 }
650 }
651
652 /**
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800653 * Stores the supplied bitmap in cache.
654 */
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800655 private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800656 if (DEBUG) {
657 BitmapHolder prev = mBitmapHolderCache.get(key);
658 if (prev != null && prev.bytes != null) {
659 Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
660 if (prev.fresh) {
661 mFreshCacheOverwrite.incrementAndGet();
662 } else {
663 mStaleCacheOverwrite.incrementAndGet();
664 }
665 }
666 Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
667 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800668 BitmapHolder holder = new BitmapHolder(bytes);
669 holder.fresh = true;
670
671 // Unless this image is being preloaded, decode it right away while
672 // we are still on the background thread.
673 if (!preloading) {
674 inflateBitmap(holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800675 }
676
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800677 mBitmapHolderCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800678 }
679
Dave Santoro14d20832011-12-02 18:14:52 -0800680 @Override
681 public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
682 Request request = Request.createFromUri(photoUri, true, false, DEFAULT_AVATER);
683 BitmapHolder holder = new BitmapHolder(photoBytes);
684 mBitmapHolderCache.put(request.getKey(), holder);
Daniel Lehmann7a46cde2012-02-01 17:53:44 -0800685 mBitmapCache.put(request.getKey(), bitmap);
Dave Santoro14d20832011-12-02 18:14:52 -0800686 }
687
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800688 /**
689 * Populates an array of photo IDs that need to be loaded.
690 */
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100691 private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
692 Set<String> photoIdsAsStrings, Set<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800693 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800694 photoIdsAsStrings.clear();
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -0700695 uris.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800696
697 /*
698 * Since the call is made from the loader thread, the map could be
699 * changing during the iteration. That's not really a problem:
700 * ConcurrentHashMap will allow those changes to happen without throwing
701 * exceptions. Since we may miss some requests in the situation of
702 * concurrent change, we will need to check the map again once loading
703 * is complete.
704 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700705 Iterator<Request> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800706 while (iterator.hasNext()) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700707 Request request = iterator.next();
708 BitmapHolder holder = mBitmapHolderCache.get(request);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800709 if (holder == null || !holder.fresh) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700710 if (request.isUriRequest()) {
711 uris.add(request.mUri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700712 } else {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700713 photoIds.add(request.mId);
714 photoIdsAsStrings.add(String.valueOf(request.mId));
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700715 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800716 }
717 }
718 }
719
720 /**
721 * The thread that performs loading of photos from the database.
722 */
723 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700724 private static final int BUFFER_SIZE = 1024*16;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800725 private static final int MESSAGE_PRELOAD_PHOTOS = 0;
726 private static final int MESSAGE_LOAD_PHOTOS = 1;
727
728 /**
729 * A pause between preload batches that yields to the UI thread.
730 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700731 private static final int PHOTO_PRELOAD_DELAY = 1000;
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800732
733 /**
734 * Number of photos to preload per batch.
735 */
736 private static final int PRELOAD_BATCH = 25;
737
738 /**
739 * Maximum number of photos to preload. If the cache size is 2Mb and
740 * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
741 */
Makoto Onuki173f2812011-09-06 14:49:27 -0700742 private static final int MAX_PHOTOS_TO_PRELOAD = 100;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700743
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800744 private final ContentResolver mResolver;
745 private final StringBuilder mStringBuilder = new StringBuilder();
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100746 private final Set<Long> mPhotoIds = Sets.newHashSet();
747 private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
748 private final Set<Uri> mPhotoUris = Sets.newHashSet();
749 private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800750
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800751 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700752 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800753
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800754 private static final int PRELOAD_STATUS_NOT_STARTED = 0;
755 private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
756 private static final int PRELOAD_STATUS_DONE = 2;
757
758 private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
759
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800760 public LoaderThread(ContentResolver resolver) {
761 super(LOADER_THREAD_NAME);
762 mResolver = resolver;
763 }
764
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800765 public void ensureHandler() {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800766 if (mLoaderThreadHandler == null) {
767 mLoaderThreadHandler = new Handler(getLooper(), this);
768 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800769 }
770
771 /**
772 * Kicks off preloading of the next batch of photos on the background thread.
773 * Preloading will happen after a delay: we want to yield to the UI thread
774 * as much as possible.
775 * <p>
776 * If preloading is already complete, does nothing.
777 */
778 public void requestPreloading() {
779 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
780 return;
781 }
782
783 ensureHandler();
784 if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
785 return;
786 }
787
788 mLoaderThreadHandler.sendEmptyMessageDelayed(
789 MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
790 }
791
792 /**
793 * Sends a message to this thread to load requested photos. Cancels a preloading
794 * request, if any: we don't want preloading to impede loading of the photos
795 * we need to display now.
796 */
797 public void requestLoading() {
798 ensureHandler();
799 mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
800 mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800801 }
802
803 /**
804 * Receives the above message, loads photos and then sends a message
805 * to the main thread to process them.
806 */
Daniel Lehmannecfc26c2011-09-12 17:44:35 -0700807 @Override
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800808 public boolean handleMessage(Message msg) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800809 switch (msg.what) {
810 case MESSAGE_PRELOAD_PHOTOS:
811 preloadPhotosInBackground();
812 break;
813 case MESSAGE_LOAD_PHOTOS:
814 loadPhotosInBackground();
815 break;
816 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800817 return true;
818 }
819
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800820 /**
821 * The first time it is called, figures out which photos need to be preloaded.
822 * Each subsequent call preloads the next batch of photos and requests
823 * another cycle of preloading after a delay. The whole process ends when
824 * we either run out of photos to preload or fill up cache.
825 */
826 private void preloadPhotosInBackground() {
827 if (mPreloadStatus == PRELOAD_STATUS_DONE) {
828 return;
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800829 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800830
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800831 if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
832 queryPhotosForPreload();
833 if (mPreloadPhotoIds.isEmpty()) {
834 mPreloadStatus = PRELOAD_STATUS_DONE;
835 } else {
836 mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
837 }
838 requestPreloading();
839 return;
840 }
841
Jesse Wilsonfb231aa2011-02-07 15:15:56 -0800842 if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800843 mPreloadStatus = PRELOAD_STATUS_DONE;
844 return;
845 }
846
847 mPhotoIds.clear();
848 mPhotoIdsAsStrings.clear();
849
850 int count = 0;
851 int preloadSize = mPreloadPhotoIds.size();
852 while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
853 preloadSize--;
854 count++;
855 Long photoId = mPreloadPhotoIds.get(preloadSize);
856 mPhotoIds.add(photoId);
857 mPhotoIdsAsStrings.add(photoId.toString());
858 mPreloadPhotoIds.remove(preloadSize);
859 }
860
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700861 loadPhotosFromDatabase(true);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800862
863 if (preloadSize == 0) {
864 mPreloadStatus = PRELOAD_STATUS_DONE;
865 }
866
Makoto Onuki173f2812011-09-06 14:49:27 -0700867 Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
868 + mBitmapHolderCache.size());
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800869
870 requestPreloading();
871 }
872
873 private void queryPhotosForPreload() {
874 Cursor cursor = null;
875 try {
876 Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
877 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700878 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
879 String.valueOf(MAX_PHOTOS_TO_PRELOAD))
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800880 .build();
881 cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
882 Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
883 null,
Daniel Lehmann4ccae562011-05-02 16:39:01 -0700884 Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800885
886 if (cursor != null) {
887 while (cursor.moveToNext()) {
888 // Insert them in reverse order, because we will be taking
889 // them from the end of the list for loading.
890 mPreloadPhotoIds.add(0, cursor.getLong(0));
891 }
892 }
893 } finally {
894 if (cursor != null) {
895 cursor.close();
896 }
897 }
898 }
899
900 private void loadPhotosInBackground() {
901 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Daniel Lehmann23b1b542011-06-10 20:01:38 -0700902 loadPhotosFromDatabase(false);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800903 loadRemotePhotos();
904 requestPreloading();
905 }
906
907 private void loadPhotosFromDatabase(boolean preloading) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100908 if (mPhotoIds.isEmpty()) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800909 return;
910 }
911
912 // Remove loaded photos from the preload queue: we don't want
913 // the preloading process to load them again.
914 if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100915 for (Long id : mPhotoIds) {
916 mPreloadPhotoIds.remove(id);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800917 }
918 if (mPreloadPhotoIds.isEmpty()) {
919 mPreloadStatus = PRELOAD_STATUS_DONE;
920 }
921 }
922
923 mStringBuilder.setLength(0);
924 mStringBuilder.append(Photo._ID + " IN(");
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100925 for (int i = 0; i < mPhotoIds.size(); i++) {
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800926 if (i != 0) {
927 mStringBuilder.append(',');
928 }
929 mStringBuilder.append('?');
930 }
931 mStringBuilder.append(')');
932
933 Cursor cursor = null;
934 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800935 if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
Dave Santoro84cac442011-08-24 15:23:10 -0700936 cursor = mResolver.query(Data.CONTENT_URI,
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800937 COLUMNS,
938 mStringBuilder.toString(),
939 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
940 null);
941
942 if (cursor != null) {
943 while (cursor.moveToNext()) {
944 Long id = cursor.getLong(0);
945 byte[] bytes = cursor.getBlob(1);
946 cacheBitmap(id, bytes, preloading);
947 mPhotoIds.remove(id);
948 }
949 }
950 } finally {
951 if (cursor != null) {
952 cursor.close();
953 }
954 }
955
Dave Santoro84cac442011-08-24 15:23:10 -0700956 // Remaining photos were not found in the contacts database (but might be in profile).
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100957 for (Long id : mPhotoIds) {
Dave Santoro84cac442011-08-24 15:23:10 -0700958 if (ContactsContract.isProfileId(id)) {
959 Cursor profileCursor = null;
960 try {
961 profileCursor = mResolver.query(
962 ContentUris.withAppendedId(Data.CONTENT_URI, id),
963 COLUMNS, null, null, null);
964 if (profileCursor != null && profileCursor.moveToFirst()) {
965 cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
966 preloading);
967 } else {
968 // Couldn't load a photo this way either.
969 cacheBitmap(id, null, preloading);
970 }
971 } finally {
972 if (profileCursor != null) {
973 profileCursor.close();
974 }
975 }
976 } else {
977 // Not a profile photo and not found - mark the cache accordingly
978 cacheBitmap(id, null, preloading);
979 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -0800980 }
981
982 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
983 }
984
985 private void loadRemotePhotos() {
Flavio Lerdad33b18c2011-07-17 22:03:15 +0100986 for (Uri uri : mPhotoUris) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700987 if (mBuffer == null) {
988 mBuffer = new byte[BUFFER_SIZE];
989 }
990 try {
Makoto Onuki8f8bd6d2011-11-08 14:09:01 -0800991 if (DEBUG) Log.d(TAG, "Loading " + uri);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700992 InputStream is = mResolver.openInputStream(uri);
993 if (is != null) {
994 ByteArrayOutputStream baos = new ByteArrayOutputStream();
995 try {
996 int size;
997 while ((size = is.read(mBuffer)) != -1) {
998 baos.write(mBuffer, 0, size);
999 }
1000 } finally {
1001 is.close();
1002 }
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001003 cacheBitmap(uri, baos.toByteArray(), false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001004 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikov0f7462c2010-10-20 14:41:18 -07001005 } else {
1006 Log.v(TAG, "Cannot load photo " + uri);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001007 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001008 }
1009 } catch (Exception ex) {
1010 Log.v(TAG, "Cannot load photo " + uri, ex);
Dmitri Plotnikov7edf2382011-01-31 16:15:48 -08001011 cacheBitmap(uri, null, false);
Dmitri Plotnikoveb689432010-09-24 10:10:57 -07001012 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -08001013 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001014 }
1015 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001016
1017 /**
1018 * A holder for either a Uri or an id and a flag whether this was requested for the dark or
1019 * light theme
1020 */
1021 private static final class Request {
1022 private final long mId;
1023 private final Uri mUri;
1024 private final boolean mDarkTheme;
1025 private final boolean mHires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001026 private final DefaultImageProvider mDefaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001027
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001028 private Request(long id, Uri uri, boolean hires, boolean darkTheme,
1029 DefaultImageProvider defaultProvider) {
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001030 mId = id;
1031 mUri = uri;
1032 mDarkTheme = darkTheme;
1033 mHires = hires;
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001034 mDefaultProvider = defaultProvider;
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001035 }
1036
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001037 public static Request createFromId(long id, boolean hires, boolean darkTheme,
1038 DefaultImageProvider defaultProvider) {
1039 return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001040 }
1041
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001042 public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
1043 DefaultImageProvider defaultProvider) {
1044 return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001045 }
1046
1047 public boolean isDarkTheme() {
1048 return mDarkTheme;
1049 }
1050
1051 public boolean isHires() {
1052 return mHires;
1053 }
1054
1055 public boolean isUriRequest() {
1056 return mUri != null;
1057 }
1058
1059 @Override
1060 public int hashCode() {
1061 if (mUri != null) return mUri.hashCode();
1062
1063 // copied over from Long.hashCode()
1064 return (int) (mId ^ (mId >>> 32));
1065 }
1066
1067 @Override
1068 public boolean equals(Object o) {
1069 if (!(o instanceof Request)) return false;
1070 final Request that = (Request) o;
1071 // Don't compare equality of mHires and mDarkTheme fields because these are only used
1072 // in the default contact photo case. When the contact does have a photo, the contact
1073 // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
1074 // the photo request on the queue twice.
1075 return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
1076 }
1077
1078 public Object getKey() {
1079 return mUri == null ? mId : mUri;
1080 }
Makoto Onuki3d3a15c2011-09-22 10:55:08 -07001081
1082 public void applyDefaultImage(ImageView view) {
1083 mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
1084 }
Daniel Lehmannecfc26c2011-09-12 17:44:35 -07001085 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -08001086}