blob: e82882caf9e77f6f176a4f9f86e2febc940ac289 [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;
26import android.os.Handler;
27import android.os.HandlerThread;
28import android.os.Message;
29import android.os.Handler.Callback;
30import android.provider.ContactsContract.Data;
31import android.provider.ContactsContract.Contacts.Photo;
32import android.widget.ImageView;
33
34import java.lang.ref.SoftReference;
35import java.util.ArrayList;
36import java.util.Iterator;
37import 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 */
45public class ContactPhotoLoader implements Callback {
46
Dmitri Plotnikov33409852010-02-20 15:02:32 -080047 private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
48
Dmitri Plotnikove8643852010-02-17 10:49:05 -080049 /**
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
Dmitri Plotnikov33409852010-02-20 15:02:32 -080063 private final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
64
Dmitri Plotnikove8643852010-02-17 10:49:05 -080065 /**
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 /**
205 * Temporarily stops loading photos from the database.
206 */
207 public void pause() {
208 mPaused = true;
209 }
210
211 /**
212 * Resumes loading photos from the database.
213 */
214 public void resume() {
215 mPaused = false;
216 if (!mPendingRequests.isEmpty()) {
217 requestLoading();
218 }
219 }
220
221 /**
222 * Sends a message to this thread itself to start loading images. If the current
223 * view contains multiple image views, all of those image views will get a chance
224 * to request their respective photos before any of those requests are executed.
225 * This allows us to load images in bulk.
226 */
227 private void requestLoading() {
228 if (!mLoadingRequested) {
229 mLoadingRequested = true;
230 mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
231 }
232 }
233
234 /**
235 * Processes requests on the main thread.
236 */
237 public boolean handleMessage(Message msg) {
238 switch (msg.what) {
239 case MESSAGE_REQUEST_LOADING: {
240 mLoadingRequested = false;
241 if (!mPaused) {
242 if (mLoaderThread == null) {
243 mLoaderThread = new LoaderThread(mContext.getContentResolver());
244 mLoaderThread.start();
245 }
246
247 mLoaderThread.requestLoading();
248 }
249 return true;
250 }
251
252 case MESSAGE_PHOTOS_LOADED: {
253 if (!mPaused) {
254 processLoadedImages();
255 }
256 return true;
257 }
258 }
259 return false;
260 }
261
262 /**
263 * Goes over pending loading requests and displays loaded photos. If some of the
264 * photos still haven't been loaded, sends another request for image loading.
265 */
266 private void processLoadedImages() {
267 Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
268 while (iterator.hasNext()) {
269 ImageView view = iterator.next();
270 long photoId = mPendingRequests.get(view);
271 boolean loaded = loadCachedPhoto(view, photoId);
272 if (loaded) {
273 iterator.remove();
274 }
275 }
276
277 if (!mPendingRequests.isEmpty()) {
278 requestLoading();
279 }
280 }
281
282 /**
283 * Stores the supplied bitmap in cache.
284 */
285 private void cacheBitmap(long id, byte[] bytes) {
286 if (mPaused) {
287 return;
288 }
289
290 BitmapHolder holder = new BitmapHolder();
291 holder.state = BitmapHolder.LOADED;
292 if (bytes != null) {
293 try {
294 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
295 holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
296 } catch (OutOfMemoryError e) {
297 // Do nothing - the photo will appear to be missing
298 }
299 }
300 mBitmapCache.put(id, holder);
301 }
302
303 /**
304 * Populates an array of photo IDs that need to be loaded.
305 */
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800306 private void obtainPhotoIdsToLoad(ArrayList<Long> photoIds,
307 ArrayList<String> photoIdsAsStrings) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800308 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800309 photoIdsAsStrings.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800310
311 /*
312 * Since the call is made from the loader thread, the map could be
313 * changing during the iteration. That's not really a problem:
314 * ConcurrentHashMap will allow those changes to happen without throwing
315 * exceptions. Since we may miss some requests in the situation of
316 * concurrent change, we will need to check the map again once loading
317 * is complete.
318 */
319 Iterator<Long> iterator = mPendingRequests.values().iterator();
320 while (iterator.hasNext()) {
321 Long id = iterator.next();
322 BitmapHolder holder = mBitmapCache.get(id);
323 if (holder.state == BitmapHolder.NEEDED) {
324 // Assuming atomic behavior
325 holder.state = BitmapHolder.LOADING;
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800326 photoIds.add(id);
327 photoIdsAsStrings.add(id.toString());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800328 }
329 }
330 }
331
332 /**
333 * The thread that performs loading of photos from the database.
334 */
335 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800336 private final ContentResolver mResolver;
337 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800338 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
339 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800340 private Handler mLoaderThreadHandler;
341
342 public LoaderThread(ContentResolver resolver) {
343 super(LOADER_THREAD_NAME);
344 mResolver = resolver;
345 }
346
347 /**
348 * Sends a message to this thread to load requested photos.
349 */
350 public void requestLoading() {
351 if (mLoaderThreadHandler == null) {
352 mLoaderThreadHandler = new Handler(getLooper(), this);
353 }
354 mLoaderThreadHandler.sendEmptyMessage(0);
355 }
356
357 /**
358 * Receives the above message, loads photos and then sends a message
359 * to the main thread to process them.
360 */
361 public boolean handleMessage(Message msg) {
362 loadPhotosFromDatabase();
363 mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
364 return true;
365 }
366
367 private void loadPhotosFromDatabase() {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800368 obtainPhotoIdsToLoad(mPhotoIds, mPhotoIdsAsStrings);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800369
370 int count = mPhotoIds.size();
371 if (count == 0) {
372 return;
373 }
374
375 mStringBuilder.setLength(0);
376 mStringBuilder.append(Photo._ID + " IN(");
377 for (int i = 0; i < count; i++) {
378 if (i != 0) {
379 mStringBuilder.append(',');
380 }
381 mStringBuilder.append('?');
382 }
383 mStringBuilder.append(')');
384
385 Cursor cursor = null;
386 try {
387 cursor = mResolver.query(Data.CONTENT_URI,
388 COLUMNS,
389 mStringBuilder.toString(),
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800390 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800391 null);
392
393 if (cursor != null) {
394 while (cursor.moveToNext()) {
395 Long id = cursor.getLong(0);
396 byte[] bytes = cursor.getBlob(1);
397 cacheBitmap(id, bytes);
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800398 mPhotoIds.remove(id);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800399 }
400 }
401 } finally {
402 if (cursor != null) {
403 cursor.close();
404 }
405 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800406
407 // Remaining photos were not found in the database - mark the cache accordingly.
408 count = mPhotoIds.size();
409 for (int i = 0; i < count; i++) {
410 cacheBitmap(mPhotoIds.get(i), null);
411 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800412 }
413 }
414}