blob: b95d6802a79da314fbb2bcb8fe01e6b3fb912047 [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
17package com.android.contacts;
18
19import com.google.android.collect.Lists;
20
21import android.content.ContentResolver;
22import android.content.Context;
23import android.database.Cursor;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070026import android.net.Uri;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080027import android.os.Handler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070028import android.os.Handler.Callback;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080029import android.os.HandlerThread;
30import android.os.Message;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080031import android.provider.ContactsContract.Contacts.Photo;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070032import android.provider.ContactsContract.Data;
33import android.util.Log;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080034import android.widget.ImageView;
35
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070036import java.io.ByteArrayOutputStream;
37import java.io.InputStream;
Dmitri Plotnikove8643852010-02-17 10:49:05 -080038import java.lang.ref.SoftReference;
39import java.util.ArrayList;
40import java.util.Iterator;
41import java.util.concurrent.ConcurrentHashMap;
42
43/**
44 * Asynchronously loads contact photos and maintains cache of photos. The class is
45 * mostly single-threaded. The only two methods accessed by the loader thread are
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070046 * {@link #cacheBitmap} and {@link #obtainPhotoIdsAndUrisToLoad}. Those methods access concurrent
Dmitri Plotnikove8643852010-02-17 10:49:05 -080047 * hash maps shared with the main thread.
48 */
49public class ContactPhotoLoader implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070050 private static final String TAG = "ContactPhotoLoader";
Dmitri Plotnikov33409852010-02-20 15:02:32 -080051 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
52
Dmitri Plotnikove8643852010-02-17 10:49:05 -080053 /**
54 * Type of message sent by the UI thread to itself to indicate that some photos
55 * need to be loaded.
56 */
57 private static final int MESSAGE_REQUEST_LOADING = 1;
58
59 /**
60 * Type of message sent by the loader thread to indicate that some photos have
61 * been loaded.
62 */
63 private static final int MESSAGE_PHOTOS_LOADED = 2;
64
65 private static final String[] EMPTY_STRING_ARRAY = new String[0];
66
Dmitri Plotnikov33409852010-02-20 15:02:32 -080067 private final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
68
Dmitri Plotnikove8643852010-02-17 10:49:05 -080069 /**
70 * The resource ID of the image to be used when the photo is unavailable or being
71 * loaded.
72 */
73 private final int mDefaultResourceId;
74
75 /**
76 * Maintains the state of a particular photo.
77 */
78 private static class BitmapHolder {
79 private static final int NEEDED = 0;
80 private static final int LOADING = 1;
81 private static final int LOADED = 2;
82
83 int state;
84 SoftReference<Bitmap> bitmapRef;
85 }
86
87 /**
88 * A soft cache for photos.
89 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070090 private final ConcurrentHashMap<Object, BitmapHolder> mBitmapCache =
91 new ConcurrentHashMap<Object, BitmapHolder>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -080092
93 /**
94 * A map from ImageView to the corresponding photo ID. Please note that this
95 * photo ID may change before the photo loading request is started.
96 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -070097 private final ConcurrentHashMap<ImageView, Object> mPendingRequests =
98 new ConcurrentHashMap<ImageView, Object>();
Dmitri Plotnikove8643852010-02-17 10:49:05 -080099
100 /**
101 * Handler for messages sent to the UI thread.
102 */
103 private final Handler mMainThreadHandler = new Handler(this);
104
105 /**
106 * Thread responsible for loading photos from the database. Created upon
107 * the first request.
108 */
109 private LoaderThread mLoaderThread;
110
111 /**
112 * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
113 */
114 private boolean mLoadingRequested;
115
116 /**
117 * Flag indicating if the image loading is paused.
118 */
119 private boolean mPaused;
120
121 private final Context mContext;
122
123 /**
124 * Constructor.
125 *
126 * @param context content context
127 * @param defaultResourceId the image resource ID to be used when there is
128 * no photo for a contact
129 */
130 public ContactPhotoLoader(Context context, int defaultResourceId) {
131 mDefaultResourceId = defaultResourceId;
132 mContext = context;
133 }
134
135 /**
136 * Load photo into the supplied image view. If the photo is already cached,
137 * it is displayed immediately. Otherwise a request is sent to load the photo
138 * from the database.
139 */
140 public void loadPhoto(ImageView view, long photoId) {
141 if (photoId == 0) {
142 // No photo is needed
143 view.setImageResource(mDefaultResourceId);
144 mPendingRequests.remove(view);
145 } else {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700146 loadPhotoByIdOrUri(view, photoId);
147 }
148 }
149
150 /**
151 * Load photo into the supplied image view. If the photo is already cached,
152 * it is displayed immediately. Otherwise a request is sent to load the photo
153 * from the location specified by the URI.
154 */
155 public void loadPhoto(ImageView view, Uri photoUri) {
156 if (photoUri == null) {
157 // No photo is needed
158 view.setImageResource(mDefaultResourceId);
159 mPendingRequests.remove(view);
160 } else {
161 loadPhotoByIdOrUri(view, photoUri);
162 }
163 }
164
165 private void loadPhotoByIdOrUri(ImageView view, Object key) {
166 boolean loaded = loadCachedPhoto(view, key);
167 if (loaded) {
168 mPendingRequests.remove(view);
169 } else {
170 mPendingRequests.put(view, key);
171 if (!mPaused) {
172 // Send a request to start loading photos
173 requestLoading();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800174 }
175 }
176 }
177
178 /**
179 * Checks if the photo is present in cache. If so, sets the photo on the view,
180 * otherwise sets the state of the photo to {@link BitmapHolder#NEEDED} and
181 * temporarily set the image to the default resource ID.
182 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700183 private boolean loadCachedPhoto(ImageView view, Object key) {
184 BitmapHolder holder = mBitmapCache.get(key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800185 if (holder == null) {
186 holder = new BitmapHolder();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700187 mBitmapCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800188 } else if (holder.state == BitmapHolder.LOADED) {
189 // Null bitmap reference means that database contains no bytes for the photo
190 if (holder.bitmapRef == null) {
191 view.setImageResource(mDefaultResourceId);
192 return true;
193 }
194
195 Bitmap bitmap = holder.bitmapRef.get();
196 if (bitmap != null) {
197 view.setImageBitmap(bitmap);
198 return true;
199 }
200
201 // Null bitmap means that the soft reference was released by the GC
202 // and we need to reload the photo.
203 holder.bitmapRef = null;
204 }
205
206 // The bitmap has not been loaded - should display the placeholder image.
207 view.setImageResource(mDefaultResourceId);
208 holder.state = BitmapHolder.NEEDED;
209 return false;
210 }
211
212 /**
213 * Stops loading images, kills the image loader thread and clears all caches.
214 */
215 public void stop() {
216 pause();
217
218 if (mLoaderThread != null) {
219 mLoaderThread.quit();
220 mLoaderThread = null;
221 }
222
223 mPendingRequests.clear();
224 mBitmapCache.clear();
225 }
226
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700227 public void clear() {
228 mPendingRequests.clear();
229 mBitmapCache.clear();
230 }
231
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800232 /**
233 * Temporarily stops loading photos from the database.
234 */
235 public void pause() {
236 mPaused = true;
237 }
238
239 /**
240 * Resumes loading photos from the database.
241 */
242 public void resume() {
243 mPaused = false;
244 if (!mPendingRequests.isEmpty()) {
245 requestLoading();
246 }
247 }
248
249 /**
250 * Sends a message to this thread itself to start loading images. If the current
251 * view contains multiple image views, all of those image views will get a chance
252 * to request their respective photos before any of those requests are executed.
253 * This allows us to load images in bulk.
254 */
255 private void requestLoading() {
256 if (!mLoadingRequested) {
257 mLoadingRequested = true;
258 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
259 }
260 }
261
262 /**
263 * Processes requests on the main thread.
264 */
265 public boolean handleMessage(Message msg) {
266 switch (msg.what) {
267 case MESSAGE_REQUEST_LOADING: {
268 mLoadingRequested = false;
269 if (!mPaused) {
270 if (mLoaderThread == null) {
271 mLoaderThread = new LoaderThread(mContext.getContentResolver());
272 mLoaderThread.start();
273 }
274
275 mLoaderThread.requestLoading();
276 }
277 return true;
278 }
279
280 case MESSAGE_PHOTOS_LOADED: {
281 if (!mPaused) {
282 processLoadedImages();
283 }
284 return true;
285 }
286 }
287 return false;
288 }
289
290 /**
291 * Goes over pending loading requests and displays loaded photos. If some of the
292 * photos still haven't been loaded, sends another request for image loading.
293 */
294 private void processLoadedImages() {
295 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
296 while (iterator.hasNext()) {
297 ImageView view = iterator.next();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700298 Object key = mPendingRequests.get(view);
299 boolean loaded = loadCachedPhoto(view, key);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800300 if (loaded) {
301 iterator.remove();
302 }
303 }
304
305 if (!mPendingRequests.isEmpty()) {
306 requestLoading();
307 }
308 }
309
310 /**
311 * Stores the supplied bitmap in cache.
312 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700313 private void cacheBitmap(Object key, byte[] bytes) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800314 if (mPaused) {
315 return;
316 }
317
318 BitmapHolder holder = new BitmapHolder();
319 holder.state = BitmapHolder.LOADED;
320 if (bytes != null) {
321 try {
322 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
323 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
324 } catch (OutOfMemoryError e) {
325 // Do nothing - the photo will appear to be missing
326 }
327 }
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700328 mBitmapCache.put(key, holder);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800329 }
330
331 /**
332 * Populates an array of photo IDs that need to be loaded.
333 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700334 private void obtainPhotoIdsAndUrisToLoad(ArrayList<Long> photoIds,
335 ArrayList<String> photoIdsAsStrings, ArrayList<Uri> uris) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800336 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800337 photoIdsAsStrings.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800338
339 /*
340 * Since the call is made from the loader thread, the map could be
341 * changing during the iteration. That's not really a problem:
342 * ConcurrentHashMap will allow those changes to happen without throwing
343 * exceptions. Since we may miss some requests in the situation of
344 * concurrent change, we will need to check the map again once loading
345 * is complete.
346 */
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700347 Iterator<Object> iterator = mPendingRequests.values().iterator();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800348 while (iterator.hasNext()) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700349 Object key = iterator.next();
350 BitmapHolder holder = mBitmapCache.get(key);
Dmitri Plotnikov92dfa112010-04-02 11:32:50 -0700351 if (holder != null && holder.state == BitmapHolder.NEEDED) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800352 // Assuming atomic behavior
353 holder.state = BitmapHolder.LOADING;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700354 if (key instanceof Long) {
355 photoIds.add((Long)key);
356 photoIdsAsStrings.add(key.toString());
357 } else {
358 uris.add((Uri)key);
359 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800360 }
361 }
362 }
363
364 /**
365 * The thread that performs loading of photos from the database.
366 */
367 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700368 private static final int BUFFER_SIZE = 1024*16;
369
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800370 private final ContentResolver mResolver;
371 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800372 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
373 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700374 private final ArrayList<Uri> mPhotoUris = Lists.newArrayList();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800375 private Handler mLoaderThreadHandler;
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700376 private byte mBuffer[];
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800377
378 public LoaderThread(ContentResolver resolver) {
379 super(LOADER_THREAD_NAME);
380 mResolver = resolver;
381 }
382
383 /**
384 * Sends a message to this thread to load requested photos.
385 */
386 public void requestLoading() {
387 if (mLoaderThreadHandler == null) {
388 mLoaderThreadHandler = new Handler(getLooper(), this);
389 }
390 mLoaderThreadHandler.sendEmptyMessage(0);
391 }
392
393 /**
394 * Receives the above message, loads photos and then sends a message
395 * to the main thread to process them.
396 */
397 public boolean handleMessage(Message msg) {
398 loadPhotosFromDatabase();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800399 return true;
400 }
401
402 private void loadPhotosFromDatabase() {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700403 obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800404
405 int count = mPhotoIds.size();
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700406 if (count != 0) {
407 mStringBuilder.setLength(0);
408 mStringBuilder.append(Photo._ID + " IN(");
409 for (int i = 0; i < count; i++) {
410 if (i != 0) {
411 mStringBuilder.append(',');
412 }
413 mStringBuilder.append('?');
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800414 }
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700415 mStringBuilder.append(')');
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800416
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700417 Cursor cursor = null;
418 try {
419 cursor = mResolver.query(Data.CONTENT_URI,
420 COLUMNS,
421 mStringBuilder.toString(),
422 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
423 null);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800424
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700425 if (cursor != null) {
426 while (cursor.moveToNext()) {
427 Long id = cursor.getLong(0);
428 byte[] bytes = cursor.getBlob(1);
429 cacheBitmap(id, bytes);
430 mPhotoIds.remove(id);
431 }
432 }
433 } finally {
434 if (cursor != null) {
435 cursor.close();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800436 }
437 }
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700438
439 // Remaining photos were not found in the database - mark the cache accordingly.
440 count = mPhotoIds.size();
441 for (int i = 0; i < count; i++) {
442 cacheBitmap(mPhotoIds.get(i), null);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800443 }
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700444 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800445 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800446
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700447 count = mPhotoUris.size();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800448 for (int i = 0; i < count; i++) {
Dmitri Plotnikoveb689432010-09-24 10:10:57 -0700449 Uri uri = mPhotoUris.get(i);
450 if (mBuffer == null) {
451 mBuffer = new byte[BUFFER_SIZE];
452 }
453 try {
454 InputStream is = mResolver.openInputStream(uri);
455 if (is != null) {
456 ByteArrayOutputStream baos = new ByteArrayOutputStream();
457 try {
458 int size;
459 while ((size = is.read(mBuffer)) != -1) {
460 baos.write(mBuffer, 0, size);
461 }
462 } finally {
463 is.close();
464 }
465 cacheBitmap(uri, baos.toByteArray());
466 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
467 }
468 } catch (Exception ex) {
469 Log.v(TAG, "Cannot load photo " + uri, ex);
470 cacheBitmap(uri, null);
471 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800472 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800473 }
474 }
475}