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