Add image to caller info

* Add various image setting method sto CallCardFragment
* Added AnimationUtils and ContactsAsyncHelper, copied unchanged from Phone

Change-Id: I6175ccc2433a5d0ad8a9bffac678a263ee65622c
diff --git a/InCallUI/res/values/ids.xml b/InCallUI/res/values/ids.xml
new file mode 100644
index 0000000..c6ad209
--- /dev/null
+++ b/InCallUI/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+    <item type="id" name="fadeState" />
+</resources>
diff --git a/InCallUI/src/com/android/incallui/AnimationUtils.java b/InCallUI/src/com/android/incallui/AnimationUtils.java
new file mode 100644
index 0000000..2bf730c
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/AnimationUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2012 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.incallui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.ImageView;
+
+/**
+ * Utilities for Animation.
+ */
+public class AnimationUtils {
+    private static final String LOG_TAG = AnimationUtils.class.getSimpleName();
+    /**
+     * Turn on when you're interested in fading animation. Intentionally untied from other debug
+     * settings.
+     */
+    private static final boolean FADE_DBG = false;
+
+    /**
+     * Duration for animations in msec, which can be used with
+     * {@link ViewPropertyAnimator#setDuration(long)} for example.
+     */
+    public static final int ANIMATION_DURATION = 250;
+
+    private AnimationUtils() {
+    }
+
+    /**
+     * Simple Utility class that runs fading animations on specified views.
+     */
+    public static class Fade {
+
+        // View tag that's set during the fade-out animation; see hide() and
+        // isFadingOut().
+        private static final int FADE_STATE_KEY = R.id.fadeState;
+        private static final String FADING_OUT = "fading_out";
+
+        /**
+         * Sets the visibility of the specified view to View.VISIBLE and then
+         * fades it in. If the view is already visible (and not in the middle
+         * of a fade-out animation), this method will return without doing
+         * anything.
+         *
+         * @param view The view to be faded in
+         */
+        public static void show(final View view) {
+            if (FADE_DBG) log("Fade: SHOW view " + view + "...");
+            if (FADE_DBG) log("Fade: - visibility = " + view.getVisibility());
+            if ((view.getVisibility() != View.VISIBLE) || isFadingOut(view)) {
+                view.animate().cancel();
+                // ...and clear the FADE_STATE_KEY tag in case we just
+                // canceled an in-progress fade-out animation.
+                view.setTag(FADE_STATE_KEY, null);
+
+                view.setAlpha(0);
+                view.setVisibility(View.VISIBLE);
+                view.animate().setDuration(ANIMATION_DURATION);
+                view.animate().alpha(1);
+                if (FADE_DBG) log("Fade: ==> SHOW " + view
+                                  + " DONE.  Set visibility = " + View.VISIBLE);
+            } else {
+                if (FADE_DBG) log("Fade: ==> Ignoring, already visible AND not fading out.");
+            }
+        }
+
+        /**
+         * Fades out the specified view and then sets its visibility to the
+         * specified value (either View.INVISIBLE or View.GONE). If the view
+         * is not currently visibile, the method will return without doing
+         * anything.
+         *
+         * Note that *during* the fade-out the view itself will still have
+         * visibility View.VISIBLE, although the isFadingOut() method will
+         * return true (in case the UI code needs to detect this state.)
+         *
+         * @param view The view to be hidden
+         * @param visibility The value to which the view's visibility will be
+         *                   set after it fades out.
+         *                   Must be either View.INVISIBLE or View.GONE.
+         */
+        public static void hide(final View view, final int visibility) {
+            if (FADE_DBG) log("Fade: HIDE view " + view + "...");
+            if (view.getVisibility() == View.VISIBLE &&
+                (visibility == View.INVISIBLE || visibility == View.GONE)) {
+
+                // Use a view tag to mark this view as being in the middle
+                // of a fade-out animation.
+                view.setTag(FADE_STATE_KEY, FADING_OUT);
+
+                view.animate().cancel();
+                view.animate().setDuration(ANIMATION_DURATION);
+                view.animate().alpha(0f).setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        view.setAlpha(1);
+                        view.setVisibility(visibility);
+                        view.animate().setListener(null);
+                        // ...and we're done with the fade-out, so clear the view tag.
+                        view.setTag(FADE_STATE_KEY, null);
+                        if (FADE_DBG) log("Fade: HIDE " + view
+                                + " DONE.  Set visibility = " + visibility);
+                    }
+                });
+            }
+        }
+
+        /**
+         * @return true if the specified view is currently in the middle
+         * of a fade-out animation.  (During the fade-out, the view's
+         * visibility is still VISIBLE, although in many cases the UI
+         * should behave as if it's already invisible or gone.  This
+         * method allows the UI code to detect that state.)
+         *
+         * @see #hide(View, int)
+         */
+        public static boolean isFadingOut(final View view) {
+            if (FADE_DBG) {
+                log("Fade: isFadingOut view " + view + "...");
+                log("Fade:   - getTag() returns: " + view.getTag(FADE_STATE_KEY));
+                log("Fade:   - returning: " + (view.getTag(FADE_STATE_KEY) == FADING_OUT));
+            }
+            return (view.getTag(FADE_STATE_KEY) == FADING_OUT);
+        }
+
+    }
+
+    /**
+     * Drawable achieving cross-fade, just like TransitionDrawable. We can have
+     * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}).
+     */
+    private static class CrossFadeDrawable extends LayerDrawable {
+        private final ObjectAnimator mAnimator;
+
+        public CrossFadeDrawable(Drawable[] layers) {
+            super(layers);
+            mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0);
+        }
+
+        private int mCrossFadeAlpha;
+
+        /**
+         * This will be used from ObjectAnimator.
+         * Note: this method is protected by proguard.flags so that it won't be removed
+         * automatically.
+         */
+        @SuppressWarnings("unused")
+        public void setCrossFadeAlpha(int alpha) {
+            mCrossFadeAlpha = alpha;
+            invalidateSelf();
+        }
+
+        public ObjectAnimator getAnimator() {
+            return mAnimator;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            Drawable first = getDrawable(0);
+            Drawable second = getDrawable(1);
+
+            if (mCrossFadeAlpha > 0) {
+                first.setAlpha(mCrossFadeAlpha);
+                first.draw(canvas);
+                first.setAlpha(255);
+            }
+
+            if (mCrossFadeAlpha < 0xff) {
+                second.setAlpha(0xff - mCrossFadeAlpha);
+                second.draw(canvas);
+                second.setAlpha(0xff);
+            }
+        }
+    }
+
+    private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) {
+        Drawable[] layers = new Drawable[2];
+        layers[0] = first;
+        layers[1] = second;
+        return new CrossFadeDrawable(layers);
+    }
+
+    /**
+     * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to"
+     * are the same.
+     */
+    public static void startCrossFade(
+            final ImageView imageView, final Drawable from, final Drawable to) {
+        // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables
+        // pointing to the same Bitmap.
+        final boolean areSameImage = from.equals(to) ||
+                ((from instanceof BitmapDrawable)
+                        && (to instanceof BitmapDrawable)
+                        && ((BitmapDrawable) from).getBitmap()
+                                .equals(((BitmapDrawable) to).getBitmap()));
+        if (!areSameImage) {
+            if (FADE_DBG) {
+                log("Start cross-fade animation for " + imageView
+                        + "(" + Integer.toHexString(from.hashCode()) + " -> "
+                        + Integer.toHexString(to.hashCode()) + ")");
+            }
+
+            CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to);
+            ObjectAnimator animator = crossFadeDrawable.getAnimator();
+            imageView.setImageDrawable(crossFadeDrawable);
+            animator.setDuration(ANIMATION_DURATION);
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    if (FADE_DBG) {
+                        log("cross-fade animation start ("
+                                + Integer.toHexString(from.hashCode()) + " -> "
+                                + Integer.toHexString(to.hashCode()) + ")");
+                    }
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (FADE_DBG) {
+                        log("cross-fade animation ended ("
+                                + Integer.toHexString(from.hashCode()) + " -> "
+                                + Integer.toHexString(to.hashCode()) + ")");
+                    }
+                    animation.removeAllListeners();
+                    // Workaround for issue 6300562; this will force the drawable to the
+                    // resultant one regardless of animation glitch.
+                    imageView.setImageDrawable(to);
+                }
+            });
+            animator.start();
+
+            /* We could use TransitionDrawable here, but it may cause some weird animation in
+             * some corner cases. See issue 6300562
+             * TODO: decide which to be used in the long run. TransitionDrawable is old but system
+             * one. Ours uses new animation framework and thus have callback (great for testing),
+             * while no framework support for the exact class.
+
+            Drawable[] layers = new Drawable[2];
+            layers[0] = from;
+            layers[1] = to;
+            TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+            imageView.setImageDrawable(transitionDrawable);
+            transitionDrawable.startTransition(ANIMATION_DURATION); */
+            imageView.setTag(to);
+        } else {
+            if (FADE_DBG) {
+                log("*Not* start cross-fade. " + imageView);
+            }
+        }
+    }
+
+    // Debugging / testing code
+
+    private static void log(String msg) {
+        Log.d(LOG_TAG, msg);
+    }
+}
\ No newline at end of file
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index 9819f15..b584d5e 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -17,12 +17,16 @@
 package com.android.incallui;
 
 import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 /**
@@ -34,6 +38,7 @@
     private TextView mPhoneNumber;
     private TextView mNumberLabel;
     private TextView mName;
+    private ImageView mPhoto;
 
 
     private ViewStub mSecondaryCallInfo;
@@ -56,6 +61,7 @@
         mName = (TextView) view.findViewById(R.id.name);
         mNumberLabel = (TextView) view.findViewById(R.id.label);
         mSecondaryCallInfo = (ViewStub) view.findViewById(R.id.secondary_call_info);
+        mPhoto = (ImageView) view.findViewById(R.id.photo);
 
         // This method call will begin the callbacks on CallCardUi. We need to ensure
         // everything needed for the callbacks is set up before this is called.
@@ -137,4 +143,29 @@
         }
     }
 
+    @Override
+    public void setImage(int resource) {
+        setImage(getActivity().getResources().getDrawable(resource));
+    }
+
+    @Override
+    public void setImage(Drawable drawable) {
+        setDrawableToImageView(mPhoto, drawable);
+    }
+
+    @Override
+    public void setImage(Bitmap bitmap) {
+        setImage(new BitmapDrawable(getActivity().getResources(), bitmap));
+    }
+
+    private void setDrawableToImageView(ImageView view, Drawable drawable) {
+        final Drawable current = view.getDrawable();
+        if (current == null) {
+            view.setImageDrawable(drawable);
+            AnimationUtils.Fade.show(view);
+        } else {
+            AnimationUtils.startCrossFade(view, current, drawable);
+            mPhoto.setVisibility(View.VISIBLE);
+        }
+    }
 }
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index f379565..06b1157 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -18,6 +18,8 @@
 
 import android.content.ContentUris;
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.ContactsContract.Contacts;
 import android.text.TextUtils;
@@ -31,11 +33,27 @@
  * Presenter for the Call Card Fragment.
  * This class listens for changes to InCallState and passes it along to the fragment.
  */
-public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
-        implements InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener {
+public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> implements
+        InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
+        ContactsAsyncHelper.OnImageLoadCompleteListener {
+
+    private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
 
     private Context mContext;
 
+    /**
+     * Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded,
+     * or a photo is already loaded.
+     */
+    private Uri mLoadingPersonUri;
+
+    // Track the state for the photo.
+    private ContactsAsyncHelper.ImageTracker mPhotoTracker;
+
+    public CallCardPresenter() {
+        mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
+    }
+
     @Override
     public void onUiReady(CallCardUi ui) {
         super.onUiReady(ui);
@@ -87,6 +105,9 @@
         void setNumberLabel(String label);
         void setName(String name);
         void setName(String name, boolean isNumber);
+        void setImage(int resource);
+        void setImage(Drawable drawable);
+        void setImage(Bitmap bitmap);
         void setSecondaryCallInfo(boolean show, String number);
     }
 
@@ -116,11 +137,9 @@
     private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation,
             boolean isPrimary) {
 
-        //Todo (klp): Either enable or get rid of this
-        // inform the state machine that we are displaying a photo.
-        //mPhotoTracker.setPhotoRequest(info);
-        //mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
-
+        // Inform the state machine that we are displaying a photo.
+        mPhotoTracker.setPhotoRequest(info);
+        mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
 
         // The actual strings we're going to display onscreen:
         String displayName;
@@ -235,10 +254,87 @@
             updateInfoUiForPrimary(displayName, displayNumber, label);
         }
 
+        // If the photoResource is filled in for the CallerInfo, (like with the
+        // Emergency Number case), then we can just set the photo image without
+        // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
+        // for cases where CallerInfo.photoResource may be set. We can also avoid
+        // the image load step if the image data is cached.
+        final CallCardUi ui = getUi();
+        if (info == null) return;
+
+        // This will only be true for emergency numbers
+        if (info.photoResource != 0) {
+            ui.setImage(info.photoResource);
+        } else if (info.isCachedPhotoCurrent) {
+            if (info.cachedPhoto != null) {
+                ui.setImage(info.cachedPhoto);
+            } else {
+                ui.setImage(R.drawable.picture_unknown);
+            }
+        } else {
+            if (personUri == null) {
+                Logger.v(this, "personUri is null. Just use unknown picture.");
+                ui.setImage(R.drawable.picture_unknown);
+            } else if (personUri.equals(mLoadingPersonUri)) {
+                Logger.v(this, "The requested Uri (" + personUri + ") is being loaded already."
+                        + " Ignore the duplicate load request.");
+            } else {
+                // Remember which person's photo is being loaded right now so that we won't issue
+                // unnecessary load request multiple times, which will mess up animation around
+                // the contact photo.
+                mLoadingPersonUri = personUri;
+
+                // Load the image with a callback to update the image state.
+                // When the load is finished, onImageLoadComplete() will be called.
+                ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+                        mContext, personUri, this, call);
+
+                // If the image load is too slow, we show a default avatar icon afterward.
+                // If it is fast enough, this message will be canceled on onImageLoadComplete().
+                // TODO (klp): Figure out if this handler is still needed.
+                // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+                // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
+            }
+        }
         // TODO (klp): Update other fields - photo, sip label, etc.
     }
 
     /**
+     * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
+     * make sure that the call state is reflected after the image is loaded.
+     */
+    @Override
+    public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+        // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+        if (mLoadingPersonUri != null) {
+            // Start sending view notification after the current request being done.
+            // New image may possibly be available from the next phone calls.
+            //
+            // TODO: may be nice to update the image view again once the newer one
+            // is available on contacts database.
+            // TODO (klp): What is this, and why does it need the write_contacts permission?
+            // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri);
+        } else {
+            // This should not happen while we need some verbose info if it happens..
+            Logger.v(this, "Person Uri isn't available while Image is successfully loaded.");
+        }
+        mLoadingPersonUri = null;
+
+        Call call = (Call) cookie;
+
+        // TODO (klp): Handle conference calls
+
+        final CallCardUi ui = getUi();
+        if (photo != null) {
+            ui.setImage(photo);
+        } else if (photoIcon != null) {
+            ui.setImage(photoIcon);
+        } else {
+            ui.setImage(R.drawable.picture_unknown);
+        }
+    }
+
+    /**
      * Updates the info portion of the call card with passed in values for the primary user.
      */
     private void updateInfoUiForPrimary(String displayName, String displayNumber, String label) {
diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
new file mode 100644
index 0000000..c9a3317
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
@@ -0,0 +1,337 @@
+/*
+ * 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 com.android.incallui;
+
+import android.app.Notification;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+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.ContactsContract.Contacts;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for loading contacts photo asynchronously.
+ */
+public class ContactsAsyncHelper {
+
+    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 token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+         * Context, Uri, OnImageLoadCompleteListener, Object)}.
+         * @param photo Drawable object obtained by the async load.
+         * @param photoIcon Bitmap object obtained by the async load.
+         * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+         * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
+         * cookie is null.
+         */
+        public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
+                Object cookie);
+    }
+
+    // constants
+    private static final int EVENT_LOAD_IMAGE = 1;
+
+    private final Handler mResultHandler = new Handler() {
+        /** Called when loading is done. */
+        @Override
+        public void handleMessage(Message msg) {
+            WorkerArgs args = (WorkerArgs) msg.obj;
+            switch (msg.arg1) {
+                case EVENT_LOAD_IMAGE:
+                    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.photo, args.photoIcon,
+                                args.cookie);
+                    }
+                    break;
+                default:
+            }
+        }
+    };
+
+    /** Handler run on a worker thread to load photo asynchronously. */
+    private static Handler sThreadHandler;
+
+    /** For forcing the system to call its constructor */
+    @SuppressWarnings("unused")
+    private static ContactsAsyncHelper sInstance;
+
+    static {
+        sInstance = new ContactsAsyncHelper();
+    }
+
+    private static final class WorkerArgs {
+        public Context context;
+        public Uri uri;
+        public Drawable photo;
+        public Bitmap photoIcon;
+        public Object cookie;
+        public OnImageLoadCompleteListener listener;
+    }
+
+    /**
+     * 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);
+        }
+
+        /**
+         * Simple setter for the CallerInfo object.
+         */
+        public void setPhotoRequest(CallerInfo info) {
+            mCurrentCallerInfo = info;
+        }
+
+        /**
+         * 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(Contacts.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);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            WorkerArgs args = (WorkerArgs) msg.obj;
+
+            switch (msg.arg1) {
+                case EVENT_LOAD_IMAGE:
+                    InputStream inputStream = null;
+                    try {
+                        try {
+                            inputStream = Contacts.openContactPhotoInputStream(
+                                    args.context.getContentResolver(), args.uri, true);
+                        } catch (Exception e) {
+                            Log.e(LOG_TAG, "Error opening photo input stream", e);
+                        }
+
+                        if (inputStream != null) {
+                            args.photo = Drawable.createFromStream(inputStream,
+                                    args.uri.toString());
+
+                            // This assumes Drawable coming from contact database is usually
+                            // BitmapDrawable and thus we can have (down)scaled version of it.
+                            args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+                            if (DBG) {
+                                Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
+                                        " token: " + msg.what + " image URI: " + args.uri);
+                            }
+                        } else {
+                            args.photo = null;
+                            args.photoIcon = null;
+                            if (DBG) {
+                                Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
+                                        " token: " + msg.what + " image URI: " + args.uri +
+                                        ", using default image.");
+                            }
+                        }
+                    } finally {
+                        if (inputStream != null) {
+                            try {
+                                inputStream.close();
+                            } catch (IOException e) {
+                                Log.e(LOG_TAG, "Unable to close input stream.", e);
+                            }
+                        }
+                    }
+                    break;
+                default:
+            }
+
+            // send the reply to the enclosing class.
+            Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
+            reply.arg1 = msg.arg1;
+            reply.obj = msg.obj;
+            reply.sendToTarget();
+        }
+
+        /**
+         * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
+         * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
+         * create a scaled Bitmap for the Drawable.
+         */
+        private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+            if (!(photo instanceof BitmapDrawable)) {
+                return null;
+            }
+            int iconSize = context.getResources()
+                    .getDimensionPixelSize(R.dimen.notification_icon_size);
+            Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+            int orgWidth = orgBitmap.getWidth();
+            int orgHeight = orgBitmap.getHeight();
+            int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+            // We want downscaled one only when the original icon is too big.
+            if (longerEdge > iconSize) {
+                float ratio = ((float) longerEdge) / iconSize;
+                int newWidth = (int) (orgWidth / ratio);
+                int newHeight = (int) (orgHeight / ratio);
+                // If the longer edge is much longer than the shorter edge, the latter may
+                // become 0 which will cause a crash.
+                if (newWidth <= 0 || newHeight <= 0) {
+                    Log.w(LOG_TAG, "Photo icon's width or height become 0.");
+                    return null;
+                }
+
+                // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+                // should be smaller than the original.
+                return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+            } else {
+                return orgBitmap;
+            }
+        }
+    }
+
+    /**
+     * Private constructor for static class
+     */
+    private ContactsAsyncHelper() {
+        HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+        thread.start();
+        sThreadHandler = new WorkerHandler(thread.getLooper());
+    }
+
+    /**
+     * Starts an asynchronous image load. After finishing the load,
+     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+     * will be called.
+     *
+     * @param token Arbitrary integer which will be returned as the first argument of
+     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+     * @param context Context object used to do the time-consuming operation.
+     * @param personUri Uri to be used to fetch the photo
+     * @param listener Callback object which will be used when the asynchronous load is done.
+     * Can be null, which means only the asynchronous load is done while there's no way to
+     * obtain the loaded photos.
+     * @param cookie Arbitrary object the caller wants to remember, which will become the
+     * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
+     * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
+     */
+    public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
+            OnImageLoadCompleteListener listener, Object cookie) {
+        // 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 (personUri == null) {
+            Log.wtf(LOG_TAG, "Uri is missing");
+            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.uri = personUri;
+        args.listener = listener;
+
+        // 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.");
+
+        // notify the thread to begin working
+        sThreadHandler.sendMessage(msg);
+    }
+
+
+}