/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts; import com.google.android.collect.Lists; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Handler.Callback; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Contacts.Photo; import android.widget.ImageView; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; /** * Asynchronously loads contact photos and maintains cache of photos. The class is * mostly single-threaded. The only two methods accessed by the loader thread are * {@link #cacheBitmap} and {@link #obtainPhotoIdsToLoad}. Those methods access concurrent * hash maps shared with the main thread. */ public class ContactPhotoLoader implements Callback { private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; /** * Type of message sent by the UI thread to itself to indicate that some photos * need to be loaded. */ private static final int MESSAGE_REQUEST_LOADING = 1; /** * Type of message sent by the loader thread to indicate that some photos have * been loaded. */ private static final int MESSAGE_PHOTOS_LOADED = 2; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO }; /** * The resource ID of the image to be used when the photo is unavailable or being * loaded. */ private final int mDefaultResourceId; /** * Maintains the state of a particular photo. */ private static class BitmapHolder { private static final int NEEDED = 0; private static final int LOADING = 1; private static final int LOADED = 2; int state; SoftReference<Bitmap> bitmapRef; } /** * A soft cache for photos. */ private final ConcurrentHashMap<Long, BitmapHolder> mBitmapCache = new ConcurrentHashMap<Long, BitmapHolder>(); /** * A map from ImageView to the corresponding photo ID. Please note that this * photo ID may change before the photo loading request is started. */ private final ConcurrentHashMap<ImageView, Long> mPendingRequests = new ConcurrentHashMap<ImageView, Long>(); /** * Handler for messages sent to the UI thread. */ private final Handler mMainThreadHandler = new Handler(this); /** * Thread responsible for loading photos from the database. Created upon * the first request. */ private LoaderThread mLoaderThread; /** * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */ private boolean mLoadingRequested; /** * Flag indicating if the image loading is paused. */ private boolean mPaused; private final Context mContext; /** * Constructor. * * @param context content context * @param defaultResourceId the image resource ID to be used when there is * no photo for a contact */ public ContactPhotoLoader(Context context, int defaultResourceId) { mDefaultResourceId = defaultResourceId; mContext = context; } /** * Load photo into the supplied image view. If the photo is already cached, * it is displayed immediately. Otherwise a request is sent to load the photo * from the database. */ public void loadPhoto(ImageView view, long photoId) { if (photoId == 0) { // No photo is needed view.setImageResource(mDefaultResourceId); mPendingRequests.remove(view); } else { boolean loaded = loadCachedPhoto(view, photoId); if (loaded) { mPendingRequests.remove(view); } else { mPendingRequests.put(view, photoId); if (!mPaused) { // Send a request to start loading photos requestLoading(); } } } } /** * Checks if the photo is present in cache. If so, sets the photo on the view, * otherwise sets the state of the photo to {@link BitmapHolder#NEEDED} and * temporarily set the image to the default resource ID. */ private boolean loadCachedPhoto(ImageView view, long photoId) { BitmapHolder holder = mBitmapCache.get(photoId); if (holder == null) { holder = new BitmapHolder(); mBitmapCache.put(photoId, holder); } else if (holder.state == BitmapHolder.LOADED) { // Null bitmap reference means that database contains no bytes for the photo if (holder.bitmapRef == null) { view.setImageResource(mDefaultResourceId); return true; } Bitmap bitmap = holder.bitmapRef.get(); if (bitmap != null) { view.setImageBitmap(bitmap); return true; } // Null bitmap means that the soft reference was released by the GC // and we need to reload the photo. holder.bitmapRef = null; } // The bitmap has not been loaded - should display the placeholder image. view.setImageResource(mDefaultResourceId); holder.state = BitmapHolder.NEEDED; return false; } /** * Stops loading images, kills the image loader thread and clears all caches. */ public void stop() { pause(); if (mLoaderThread != null) { mLoaderThread.quit(); mLoaderThread = null; } mPendingRequests.clear(); mBitmapCache.clear(); } public void clear() { mPendingRequests.clear(); mBitmapCache.clear(); } /** * Temporarily stops loading photos from the database. */ public void pause() { mPaused = true; } /** * Resumes loading photos from the database. */ public void resume() { mPaused = false; if (!mPendingRequests.isEmpty()) { requestLoading(); } } /** * Sends a message to this thread itself to start loading images. If the current * view contains multiple image views, all of those image views will get a chance * to request their respective photos before any of those requests are executed. * This allows us to load images in bulk. */ private void requestLoading() { if (!mLoadingRequested) { mLoadingRequested = true; mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); } } /** * Processes requests on the main thread. */ public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_REQUEST_LOADING: { mLoadingRequested = false; if (!mPaused) { if (mLoaderThread == null) { mLoaderThread = new LoaderThread(mContext.getContentResolver()); mLoaderThread.start(); } mLoaderThread.requestLoading(); } return true; } case MESSAGE_PHOTOS_LOADED: { if (!mPaused) { processLoadedImages(); } return true; } } return false; } /** * Goes over pending loading requests and displays loaded photos. If some of the * photos still haven't been loaded, sends another request for image loading. */ private void processLoadedImages() { Iterator<ImageView> iterator = mPendingRequests.keySet().iterator(); while (iterator.hasNext()) { ImageView view = iterator.next(); long photoId = mPendingRequests.get(view); boolean loaded = loadCachedPhoto(view, photoId); if (loaded) { iterator.remove(); } } if (!mPendingRequests.isEmpty()) { requestLoading(); } } /** * Stores the supplied bitmap in cache. */ private void cacheBitmap(long id, byte[] bytes) { if (mPaused) { return; } BitmapHolder holder = new BitmapHolder(); holder.state = BitmapHolder.LOADED; if (bytes != null) { try { Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null); holder.bitmapRef = new SoftReference<Bitmap>(bitmap); } catch (OutOfMemoryError e) { // Do nothing - the photo will appear to be missing } } mBitmapCache.put(id, holder); } /** * Populates an array of photo IDs that need to be loaded. */ private void obtainPhotoIdsToLoad(ArrayList<Long> photoIds, ArrayList<String> photoIdsAsStrings) { photoIds.clear(); photoIdsAsStrings.clear(); /* * Since the call is made from the loader thread, the map could be * changing during the iteration. That's not really a problem: * ConcurrentHashMap will allow those changes to happen without throwing * exceptions. Since we may miss some requests in the situation of * concurrent change, we will need to check the map again once loading * is complete. */ Iterator<Long> iterator = mPendingRequests.values().iterator(); while (iterator.hasNext()) { Long id = iterator.next(); BitmapHolder holder = mBitmapCache.get(id); if (holder != null && holder.state == BitmapHolder.NEEDED) { // Assuming atomic behavior holder.state = BitmapHolder.LOADING; photoIds.add(id); photoIdsAsStrings.add(id.toString()); } } } /** * The thread that performs loading of photos from the database. */ private class LoaderThread extends HandlerThread implements Callback { private final ContentResolver mResolver; private final StringBuilder mStringBuilder = new StringBuilder(); private final ArrayList<Long> mPhotoIds = Lists.newArrayList(); private final ArrayList<String> mPhotoIdsAsStrings = Lists.newArrayList(); private Handler mLoaderThreadHandler; public LoaderThread(ContentResolver resolver) { super(LOADER_THREAD_NAME); mResolver = resolver; } /** * Sends a message to this thread to load requested photos. */ public void requestLoading() { if (mLoaderThreadHandler == null) { mLoaderThreadHandler = new Handler(getLooper(), this); } mLoaderThreadHandler.sendEmptyMessage(0); } /** * Receives the above message, loads photos and then sends a message * to the main thread to process them. */ public boolean handleMessage(Message msg) { loadPhotosFromDatabase(); mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); return true; } private void loadPhotosFromDatabase() { obtainPhotoIdsToLoad(mPhotoIds, mPhotoIdsAsStrings); int count = mPhotoIds.size(); if (count == 0) { return; } mStringBuilder.setLength(0); mStringBuilder.append(Photo._ID + " IN("); for (int i = 0; i < count; i++) { if (i != 0) { mStringBuilder.append(','); } mStringBuilder.append('?'); } mStringBuilder.append(')'); Cursor cursor = null; try { cursor = mResolver.query(Data.CONTENT_URI, COLUMNS, mStringBuilder.toString(), mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), null); if (cursor != null) { while (cursor.moveToNext()) { Long id = cursor.getLong(0); byte[] bytes = cursor.getBlob(1); cacheBitmap(id, bytes); mPhotoIds.remove(id); } } } finally { if (cursor != null) { cursor.close(); } } // Remaining photos were not found in the database - mark the cache accordingly. count = mPhotoIds.size(); for (int i = 0; i < count; i++) { cacheBitmap(mPhotoIds.get(i), null); } } } }