1 /* 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 17 package com.android.contacts; 18 19 import com.google.android.collect.Lists; 20 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.os.Handler; 27 import android.os.HandlerThread; 28 import android.os.Message; 29 import android.os.Handler.Callback; 30 import android.provider.ContactsContract.Data; 31 import android.provider.ContactsContract.Contacts.Photo; 32 import android.widget.ImageView; 33 34 import java.lang.ref.SoftReference; 35 import java.util.ArrayList; 36 import java.util.Iterator; 37 import java.util.concurrent.ConcurrentHashMap; 38 39 /** 40 * Asynchronously loads contact photos and maintains cache of photos. The class is 41 * mostly single-threaded. The only two methods accessed by the loader thread are 42 * {@link #cacheBitmap} and {@link #obtainPhotoIdsToLoad}. Those methods access concurrent 43 * hash maps shared with the main thread. 44 */ 45 public class ContactPhotoLoader implements Callback { 46 47 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; 48 49 /** 50 * Type of message sent by the UI thread to itself to indicate that some photos 51 * need to be loaded. 52 */ 53 private static final int MESSAGE_REQUEST_LOADING = 1; 54 55 /** 56 * Type of message sent by the loader thread to indicate that some photos have 57 * been loaded. 58 */ 59 private static final int MESSAGE_PHOTOS_LOADED = 2; 60 61 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 62 63 private final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO }; 64 65 /** 66 * The resource ID of the image to be used when the photo is unavailable or being 67 * loaded. 68 */ 69 private final int mDefaultResourceId; 70 71 /** 72 * Maintains the state of a particular photo. 73 */ 74 private static class BitmapHolder { 75 private static final int NEEDED = 0; 76 private static final int LOADING = 1; 77 private static final int LOADED = 2; 78 79 int state; 80 SoftReference<Bitmap> bitmapRef; 81 } 82 83 /** 84 * A soft cache for photos. 85 */ 86 private final ConcurrentHashMap<Long, BitmapHolder> mBitmapCache = 87 new ConcurrentHashMap<Long, BitmapHolder>(); 88 89 /** 90 * A map from ImageView to the corresponding photo ID. Please note that this 91 * photo ID may change before the photo loading request is started. 92 */ 93 private final ConcurrentHashMap<ImageView, Long> mPendingRequests = 94 new ConcurrentHashMap<ImageView, Long>(); 95 96 /** 97 * Handler for messages sent to the UI thread. 98 */ 99 private final Handler mMainThreadHandler = new Handler(this); 100 101 /** 102 * Thread responsible for loading photos from the database. Created upon 103 * the first request. 104 */ 105 private LoaderThread mLoaderThread; 106 107 /** 108 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. 109 */ 110 private boolean mLoadingRequested; 111 112 /** 113 * Flag indicating if the image loading is paused. 114 */ 115 private boolean mPaused; 116 117 private final Context mContext; 118 119 /** 120 * Constructor. 121 * 122 * @param context content context 123 * @param defaultResourceId the image resource ID to be used when there is 124 * no photo for a contact 125 */ 126 public ContactPhotoLoader(Context context, int defaultResourceId) { 127 mDefaultResourceId = defaultResourceId; 128 mContext = context; 129 } 130 131 /** 132 * Load photo into the supplied image view. If the photo is already cached, 133 * it is displayed immediately. Otherwise a request is sent to load the photo 134 * from the database. 135 */ 136 public void loadPhoto(ImageView view, long photoId) { 137 if (photoId == 0) { 138 // No photo is needed 139 view.setImageResource(mDefaultResourceId); 140 mPendingRequests.remove(view); 141 } else { 142 boolean loaded = loadCachedPhoto(view, photoId); 143 if (loaded) { 144 mPendingRequests.remove(view); 145 } else { 146 mPendingRequests.put(view, photoId); 147 if (!mPaused) { 148 // Send a request to start loading photos 149 requestLoading(); 150 } 151 } 152 } 153 } 154 155 /** 156 * Checks if the photo is present in cache. If so, sets the photo on the view, 157 * otherwise sets the state of the photo to {@link BitmapHolder#NEEDED} and 158 * temporarily set the image to the default resource ID. 159 */ 160 private boolean loadCachedPhoto(ImageView view, long photoId) { 161 BitmapHolder holder = mBitmapCache.get(photoId); 162 if (holder == null) { 163 holder = new BitmapHolder(); 164 mBitmapCache.put(photoId, holder); 165 } else if (holder.state == BitmapHolder.LOADED) { 166 // Null bitmap reference means that database contains no bytes for the photo 167 if (holder.bitmapRef == null) { 168 view.setImageResource(mDefaultResourceId); 169 return true; 170 } 171 172 Bitmap bitmap = holder.bitmapRef.get(); 173 if (bitmap != null) { 174 view.setImageBitmap(bitmap); 175 return true; 176 } 177 178 // Null bitmap means that the soft reference was released by the GC 179 // and we need to reload the photo. 180 holder.bitmapRef = null; 181 } 182 183 // The bitmap has not been loaded - should display the placeholder image. 184 view.setImageResource(mDefaultResourceId); 185 holder.state = BitmapHolder.NEEDED; 186 return false; 187 } 188 189 /** 190 * Stops loading images, kills the image loader thread and clears all caches. 191 */ 192 public void stop() { 193 pause(); 194 195 if (mLoaderThread != null) { 196 mLoaderThread.quit(); 197 mLoaderThread = null; 198 } 199 200 mPendingRequests.clear(); 201 mBitmapCache.clear(); 202 } 203 204 public void clear() { 205 mPendingRequests.clear(); 206 mBitmapCache.clear(); 207 } 208 209 /** 210 * Temporarily stops loading photos from the database. 211 */ 212 public void pause() { 213 mPaused = true; 214 } 215 216 /** 217 * Resumes loading photos from the database. 218 */ 219 public void resume() { 220 mPaused = false; 221 if (!mPendingRequests.isEmpty()) { 222 requestLoading(); 223 } 224 } 225 226 /** 227 * Sends a message to this thread itself to start loading images. If the current 228 * view contains multiple image views, all of those image views will get a chance 229 * to request their respective photos before any of those requests are executed. 230 * This allows us to load images in bulk. 231 */ 232 private void requestLoading() { 233 if (!mLoadingRequested) { 234 mLoadingRequested = true; 235 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); 236 } 237 } 238 239 /** 240 * Processes requests on the main thread. 241 */ 242 public boolean handleMessage(Message msg) { 243 switch (msg.what) { 244 case MESSAGE_REQUEST_LOADING: { 245 mLoadingRequested = false; 246 if (!mPaused) { 247 if (mLoaderThread == null) { 248 mLoaderThread = new LoaderThread(mContext.getContentResolver()); 249 mLoaderThread.start(); 250 } 251 252 mLoaderThread.requestLoading(); 253 } 254 return true; 255 } 256 257 case MESSAGE_PHOTOS_LOADED: { 258 if (!mPaused) { 259 processLoadedImages(); 260 } 261 return true; 262 } 263 } 264 return false; 265 } 266 267 /** 268 * Goes over pending loading requests and displays loaded photos. If some of the 269 * photos still haven't been loaded, sends another request for image loading. 270 */ 271 private void processLoadedImages() { 272 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator(); 273 while (iterator.hasNext()) { 274 ImageView view = iterator.next(); 275 long photoId = mPendingRequests.get(view); 276 boolean loaded = loadCachedPhoto(view, photoId); 277 if (loaded) { 278 iterator.remove(); 279 } 280 } 281 282 if (!mPendingRequests.isEmpty()) { 283 requestLoading(); 284 } 285 } 286 287 /** 288 * Stores the supplied bitmap in cache. 289 */ 290 private void cacheBitmap(long id, byte[] bytes) { 291 if (mPaused) { 292 return; 293 } 294 295 BitmapHolder holder = new BitmapHolder(); 296 holder.state = BitmapHolder.LOADED; 297 if (bytes != null) { 298 try { 299 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null); 300 holder.bitmapRef = new SoftReference<Bitmap>(bitmap); 301 } catch (OutOfMemoryError e) { 302 // Do nothing - the photo will appear to be missing 303 } 304 } 305 mBitmapCache.put(id, holder); 306 } 307 308 /** 309 * Populates an array of photo IDs that need to be loaded. 310 */ 311 private void obtainPhotoIdsToLoad(ArrayList<Long> photoIds, 312 ArrayList<String> photoIdsAsStrings) { 313 photoIds.clear(); 314 photoIdsAsStrings.clear(); 315 316 /* 317 * Since the call is made from the loader thread, the map could be 318 * changing during the iteration. That's not really a problem: 319 * ConcurrentHashMap will allow those changes to happen without throwing 320 * exceptions. Since we may miss some requests in the situation of 321 * concurrent change, we will need to check the map again once loading 322 * is complete. 323 */ 324 Iterator<Long> iterator = mPendingRequests.values().iterator(); 325 while (iterator.hasNext()) { 326 Long id = iterator.next(); 327 BitmapHolder holder = mBitmapCache.get(id); 328 if (holder != null && holder.state == BitmapHolder.NEEDED) { 329 // Assuming atomic behavior 330 holder.state = BitmapHolder.LOADING; 331 photoIds.add(id); 332 photoIdsAsStrings.add(id.toString()); 333 } 334 } 335 } 336 337 /** 338 * The thread that performs loading of photos from the database. 339 */ 340 private class LoaderThread extends HandlerThread implements Callback { 341 private final ContentResolver mResolver; 342 private final StringBuilder mStringBuilder = new StringBuilder(); 343 private final ArrayList<Long> mPhotoIds = Lists.newArrayList(); 344 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList(); 345 private Handler mLoaderThreadHandler; 346 347 public LoaderThread(ContentResolver resolver) { 348 super(LOADER_THREAD_NAME); 349 mResolver = resolver; 350 } 351 352 /** 353 * Sends a message to this thread to load requested photos. 354 */ 355 public void requestLoading() { 356 if (mLoaderThreadHandler == null) { 357 mLoaderThreadHandler = new Handler(getLooper(), this); 358 } 359 mLoaderThreadHandler.sendEmptyMessage(0); 360 } 361 362 /** 363 * Receives the above message, loads photos and then sends a message 364 * to the main thread to process them. 365 */ 366 public boolean handleMessage(Message msg) { 367 loadPhotosFromDatabase(); 368 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); 369 return true; 370 } 371 372 private void loadPhotosFromDatabase() { 373 obtainPhotoIdsToLoad(mPhotoIds, mPhotoIdsAsStrings); 374 375 int count = mPhotoIds.size(); 376 if (count == 0) { 377 return; 378 } 379 380 mStringBuilder.setLength(0); 381 mStringBuilder.append(Photo._ID + " IN("); 382 for (int i = 0; i < count; i++) { 383 if (i != 0) { 384 mStringBuilder.append(','); 385 } 386 mStringBuilder.append('?'); 387 } 388 mStringBuilder.append(')'); 389 390 Cursor cursor = null; 391 try { 392 cursor = mResolver.query(Data.CONTENT_URI, 393 COLUMNS, 394 mStringBuilder.toString(), 395 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), 396 null); 397 398 if (cursor != null) { 399 while (cursor.moveToNext()) { 400 Long id = cursor.getLong(0); 401 byte[] bytes = cursor.getBlob(1); 402 cacheBitmap(id, bytes); 403 mPhotoIds.remove(id); 404 } 405 } 406 } finally { 407 if (cursor != null) { 408 cursor.close(); 409 } 410 } 411 412 // Remaining photos were not found in the database - mark the cache accordingly. 413 count = mPhotoIds.size(); 414 for (int i = 0; i < count; i++) { 415 cacheBitmap(mPhotoIds.get(i), null); 416 } 417 } 418 } 419 } 420