blob: 96f55a62ad31b13950d25719807485b4dfcf8182 [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
Dmitri Plotnikovb369c492010-03-24 18:11:24 -0700204 public void clear() {
205 mPendingRequests.clear();
206 mBitmapCache.clear();
207 }
208
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800209 /**
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 */
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800311 private void obtainPhotoIdsToLoad(ArrayList<Long> photoIds,
312 ArrayList<String> photoIdsAsStrings) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800313 photoIds.clear();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800314 photoIdsAsStrings.clear();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800315
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);
Dmitri Plotnikov92dfa112010-04-02 11:32:50 -0700328 if (holder != null && holder.state == BitmapHolder.NEEDED) {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800329 // Assuming atomic behavior
330 holder.state = BitmapHolder.LOADING;
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800331 photoIds.add(id);
332 photoIdsAsStrings.add(id.toString());
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800333 }
334 }
335 }
336
337 /**
338 * The thread that performs loading of photos from the database.
339 */
340 private class LoaderThread extends HandlerThread implements Callback {
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800341 private final ContentResolver mResolver;
342 private final StringBuilder mStringBuilder = new StringBuilder();
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800343 private final ArrayList<Long> mPhotoIds = Lists.newArrayList();
344 private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList();
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800345 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() {
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800373 obtainPhotoIdsToLoad(mPhotoIds, mPhotoIdsAsStrings);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800374
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(),
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800395 mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800396 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);
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800403 mPhotoIds.remove(id);
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800404 }
405 }
406 } finally {
407 if (cursor != null) {
408 cursor.close();
409 }
410 }
Dmitri Plotnikov33409852010-02-20 15:02:32 -0800411
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 }
Dmitri Plotnikove8643852010-02-17 10:49:05 -0800417 }
418 }
419}