/* * Copyright (C) 2008 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 android.pim; import com.android.internal.telephony.CallerInfo; import com.android.internal.telephony.Connection; import android.content.ContentUris; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.provider.Contacts; import android.provider.Contacts.People; import android.util.Log; import android.view.View; import android.widget.ImageView; import java.io.InputStream; /** * Helper class for async access of images. */ public class ContactsAsyncHelper extends Handler { private static final boolean DBG = false; private static final String LOG_TAG = "ContactsAsyncHelper"; /** * Interface for a WorkerHandler result return. */ public interface OnImageLoadCompleteListener { /** * Called when the image load is complete. * * @param imagePresent true if an image was found */ public void onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent); } // constants private static final int EVENT_LOAD_IMAGE = 1; private static final int DEFAULT_TOKEN = -1; // static objects private static Handler sThreadHandler; private static ContactsAsyncHelper sInstance; static { sInstance = new ContactsAsyncHelper(); } private static final class WorkerArgs { public Context context; public ImageView view; public Uri uri; public int defaultResource; public Object result; public Object cookie; public OnImageLoadCompleteListener listener; public CallerInfo info; } /** * public inner class to help out the ContactsAsyncHelper callers * with tracking the state of the CallerInfo Queries and image * loading. * * Logic contained herein is used to remove the race conditions * that exist as the CallerInfo queries run and mix with the image * loads, which then mix with the Phone state changes. */ public static class ImageTracker { // Image display states public static final int DISPLAY_UNDEFINED = 0; public static final int DISPLAY_IMAGE = -1; public static final int DISPLAY_DEFAULT = -2; // State of the image on the imageview. private CallerInfo mCurrentCallerInfo; private int displayMode; public ImageTracker() { mCurrentCallerInfo = null; displayMode = DISPLAY_UNDEFINED; } /** * Used to see if the requested call / connection has a * different caller attached to it than the one we currently * have in the CallCard. */ public boolean isDifferentImageRequest(CallerInfo ci) { // note, since the connections are around for the lifetime of the // call, and the CallerInfo-related items as well, we can // definitely use a simple != comparison. return (mCurrentCallerInfo != ci); } public boolean isDifferentImageRequest(Connection connection) { // if the connection does not exist, see if the // mCurrentCallerInfo is also null to match. if (connection == null) { if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null"); return (mCurrentCallerInfo != null); } Object o = connection.getUserData(); // if the call does NOT have a callerInfo attached // then it is ok to query. boolean runQuery = true; if (o instanceof CallerInfo) { runQuery = isDifferentImageRequest((CallerInfo) o); } return runQuery; } /** * Simple setter for the CallerInfo object. */ public void setPhotoRequest(CallerInfo ci) { mCurrentCallerInfo = ci; } /** * Convenience method used to retrieve the URI * representing the Photo file recorded in the attached * CallerInfo Object. */ public Uri getPhotoUri() { if (mCurrentCallerInfo != null) { return ContentUris.withAppendedId(People.CONTENT_URI, mCurrentCallerInfo.person_id); } return null; } /** * Simple setter for the Photo state. */ public void setPhotoState(int state) { displayMode = state; } /** * Simple getter for the Photo state. */ public int getPhotoState() { return displayMode; } } /** * Thread worker class that handles the task of opening the stream and loading * the images. */ private class WorkerHandler extends Handler { public WorkerHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { WorkerArgs args = (WorkerArgs) msg.obj; switch (msg.arg1) { case EVENT_LOAD_IMAGE: InputStream inputStream = Contacts.People.openContactPhotoInputStream( args.context.getContentResolver(), args.uri); if (inputStream != null) { args.result = Drawable.createFromStream(inputStream, args.uri.toString()); if (DBG) Log.d(LOG_TAG, "Loading image: " + msg.arg1 + " token: " + msg.what + " image URI: " + args.uri); } else { args.result = null; if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + " token: " + msg.what + " image URI: " + args.uri + ", using default image."); } break; default: } // send the reply to the enclosing class. Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what); reply.arg1 = msg.arg1; reply.obj = msg.obj; reply.sendToTarget(); } } /** * Private constructor for static class */ private ContactsAsyncHelper() { HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); thread.start(); sThreadHandler = new WorkerHandler(thread.getLooper()); } /** * Convenience method for calls that do not want to deal with listeners and tokens. */ public static final void updateImageViewWithContactPhotoAsync(Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } /** * Convenience method for calls that do not want to deal with listeners and tokens, but have * a CallerInfo object to cache the image to. */ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } /** * Start an image load, attach the result to the specified CallerInfo object. * Note, when the query is started, we make the ImageView INVISIBLE if the * placeholderImageResource value is -1. When we're given a valid (!= -1) * placeholderImageResource value, we make sure the image is visible. */ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, OnImageLoadCompleteListener listener, Object cookie, Context context, ImageView imageView, Uri person, int placeholderImageResource) { // in case the source caller info is null, the URI will be null as well. // just update using the placeholder image in this case. if (person == null) { if (DBG) Log.d(LOG_TAG, "target image is null, just display placeholder."); imageView.setVisibility(View.VISIBLE); imageView.setImageResource(placeholderImageResource); return; } // Added additional Cookie field in the callee to handle arguments // sent to the callback function. // setup arguments WorkerArgs args = new WorkerArgs(); args.cookie = cookie; args.context = context; args.view = imageView; args.uri = person; args.defaultResource = placeholderImageResource; args.listener = listener; args.info = info; // setup message arguments Message msg = sThreadHandler.obtainMessage(token); msg.arg1 = EVENT_LOAD_IMAGE; msg.obj = args; if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + ", displaying default image for now."); // set the default image first, when the query is complete, we will // replace the image with the correct one. if (placeholderImageResource != -1) { imageView.setVisibility(View.VISIBLE); imageView.setImageResource(placeholderImageResource); } else { imageView.setVisibility(View.INVISIBLE); } // notify the thread to begin working sThreadHandler.sendMessage(msg); } /** * Called when loading is done. */ @Override public void handleMessage(Message msg) { WorkerArgs args = (WorkerArgs) msg.obj; switch (msg.arg1) { case EVENT_LOAD_IMAGE: boolean imagePresent = false; // if the image has been loaded then display it, otherwise set default. // in either case, make sure the image is visible. if (args.result != null) { args.view.setVisibility(View.VISIBLE); args.view.setImageDrawable((Drawable) args.result); // make sure the cached photo data is updated. if (args.info != null) { args.info.cachedPhoto = (Drawable) args.result; } imagePresent = true; } else if (args.defaultResource != -1) { args.view.setVisibility(View.VISIBLE); args.view.setImageResource(args.defaultResource); } // Note that the data is cached. if (args.info != null) { args.info.isCachedPhotoCurrent = true; } // notify the listener if it is there. if (args.listener != null) { if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + " image: " + args.uri + " completed"); args.listener.onImageLoadComplete(msg.what, args.cookie, args.view, imagePresent); } break; default: } } }