Merge "Set privileged to false." into klp-dev
diff --git a/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..7e7bc3e
--- /dev/null
+++ b/InCallUI/res/drawable-hdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..18de248
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..60f665d
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..2cbcb5f
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..20ff4b6
--- /dev/null
+++ b/InCallUI/res/drawable-mdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png
new file mode 100644
index 0000000..a6748d8
--- /dev/null
+++ b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call_on_hold.png
Binary files differ
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index b4fc852..552a42a 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -46,6 +46,7 @@
     private TextView mCallStateLabel;
     private ViewStub mSecondaryCallInfo;
     private TextView mSecondaryCallName;
+    private ImageView mSecondaryPhoto;
 
     // Cached DisplayMetrics density.
     private float mDensity;
@@ -94,59 +95,67 @@
     }
 
     @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        getPresenter().setContext(activity);
-    }
+    public void setPrimary(String number, String name, String label, Drawable photo) {
+        boolean nameIsNumber = false;
 
-    @Override
-    public void setSecondaryCallInfo(boolean show, String number) {
-        if (show) {
-            showAndInitializeSecondaryCallInfo();
-
-            // Until we have the name source, use the number as the main text for secondary calls.
-            mSecondaryCallName.setText(number);
-        } else {
-            mSecondaryCallInfo.setVisibility(View.GONE);
+        // If there is no name, then use the number as the name;
+        if (TextUtils.isEmpty(name)) {
+            name = number;
+            number = null;
+            nameIsNumber = true;
         }
-    }
 
-    @Override
-    public void setNumber(String number)  {
-        if (!TextUtils.isEmpty(number)) {
+        // Set the number
+        if (TextUtils.isEmpty(number)) {
+            mPhoneNumber.setText("");
+            mPhoneNumber.setVisibility(View.GONE);
+        } else {
             mPhoneNumber.setText(number);
             mPhoneNumber.setVisibility(View.VISIBLE);
-            // We have a real phone number as "mPhoneNumber" so make it always LTR
             mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
-        } else {
-            mPhoneNumber.setVisibility(View.GONE);
         }
-    }
 
-    @Override
-    public void setName(String name, boolean isNumber) {
-        mName.setText(name);
-        mName.setVisibility(View.VISIBLE);
-        if (isNumber) {
-            mName.setTextDirection(View.TEXT_DIRECTION_LTR);
+        // Set direction of the name field
+
+        // set the name field.
+        if (TextUtils.isEmpty(name)) {
+            mName.setText("");
         } else {
-            mName.setTextDirection(View.TEXT_DIRECTION_INHERIT);
+            mName.setText(name);
+
+            int nameDirection = View.TEXT_DIRECTION_INHERIT;
+            if (nameIsNumber) {
+                nameDirection = View.TEXT_DIRECTION_LTR;
+            }
+            mName.setTextDirection(nameDirection);
         }
-    }
 
-    @Override
-    public void setName(String name) {
-        setName(name, false);
-    }
-
-    @Override
-    public void setNumberLabel(String label) {
+        // Set the label (Mobile, Work, etc)
         if (!TextUtils.isEmpty(label)) {
             mNumberLabel.setText(label);
             mNumberLabel.setVisibility(View.VISIBLE);
         } else {
             mNumberLabel.setVisibility(View.GONE);
         }
+
+        setDrawableToImageView(mPhoto, photo);
+    }
+
+    @Override
+    public void setSecondary(boolean show, String number, String name, String label,
+            Drawable photo) {
+
+        if (show) {
+            showAndInitializeSecondaryCallInfo();
+            if (TextUtils.isEmpty(name)) {
+                name = number;
+            }
+
+            mSecondaryCallName.setText(name);
+            setDrawableToImageView(mSecondaryPhoto, photo);
+        } else {
+            mSecondaryCallInfo.setVisibility(View.GONE);
+        }
     }
 
     @Override
@@ -180,6 +189,21 @@
         }
     }
 
+    private void setDrawableToImageView(ImageView view, Drawable photo) {
+        if (photo == null) {
+            photo = view.getResources().getDrawable(R.drawable.picture_unknown);
+        }
+
+        final Drawable current = view.getDrawable();
+        if (current == null) {
+            view.setImageDrawable(photo);
+            AnimationUtils.Fade.show(view);
+        } else {
+            AnimationUtils.startCrossFade(view, current, photo);
+            mPhoto.setVisibility(View.VISIBLE);
+        }
+    }
+
     private void setBluetoothOn(boolean onOff) {
         // Also, display a special icon (alongside the "Incoming call"
         // label) if there's an incoming call and audio will be routed
@@ -323,31 +347,8 @@
         if (mSecondaryCallName == null) {
             mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName);
         }
-    }
-
-    @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);
+        if (mSecondaryPhoto == null) {
+            mSecondaryPhoto = (ImageView) getView().findViewById(R.id.secondaryCallPhoto);
         }
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index d4a39ea..8a855c1 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -16,15 +16,12 @@
 
 package com.android.incallui;
 
-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;
 
 import com.android.incallui.AudioModeProvider.AudioModeListener;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
 
@@ -35,27 +32,17 @@
  * 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,
-        ContactsAsyncHelper.OnImageLoadCompleteListener, AudioModeListener {
+public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
+        implements InCallStateListener, AudioModeListener, ContactInfoCacheCallback {
 
-    private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
-
-    private Context mContext;
     private AudioModeProvider mAudioModeProvider;
+    private ContactInfoCache mContactInfoCache;
     private Call mPrimary;
-
-    /**
-     * 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;
+    private Call mSecondary;
+    private ContactCacheEntry mPrimaryContactInfo;
+    private ContactCacheEntry mSecondaryContactInfo;
 
     public CallCardPresenter() {
-        mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
     }
 
     @Override
@@ -75,10 +62,13 @@
             mAudioModeProvider.removeListener(this);
         }
         mPrimary = null;
+        mPrimaryContactInfo = null;
+        mSecondaryContactInfo = null;
     }
 
-    public void setContext(Context context) {
-        mContext = context;
+    public void setContactInfoCache(ContactInfoCache cache) {
+        mContactInfoCache = cache;
+        startContactInfoSearch();
     }
 
     @Override
@@ -107,32 +97,23 @@
         Logger.d(this, "Primary call: " + primary);
         Logger.d(this, "Secondary call: " + secondary);
 
+        mPrimary = primary;
+        mSecondary = secondary;
 
-        if (primary != null) {
-            // Set primary call data
-            final CallerInfo primaryCallInfo = CallerInfoUtils.getCallerInfoForCall(mContext,
-                    primary, null, this);
-            updateDisplayByCallerInfo(primary, primaryCallInfo, primary.getNumberPresentation(),
-                    true);
+        // Query for contact data. This will call back on onContactInfoComplete at least once
+        // synchronously, and potentially a second time asynchronously if it needs to make
+        // a full query for the data.
+        // It is in that callback that we set the values into the Ui.
+        startContactInfoSearch();
 
+        // Set the call state
+        if (mPrimary != null) {
             final boolean bluetoothOn = mAudioModeProvider != null &&
                     mAudioModeProvider.getAudioMode() == AudioMode.BLUETOOTH;
-
-            ui.setNumber(primary.getNumber());
-            ui.setCallState(primary.getState(), primary.getDisconnectCause(), bluetoothOn);
+            ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn);
         } else {
-            ui.setNumber("");
             ui.setCallState(Call.State.INVALID, Call.DisconnectCause.UNKNOWN, false);
         }
-
-        // Set secondary call data
-        if (secondary != null) {
-            ui.setSecondaryCallInfo(true, secondary.getNumber());
-        } else {
-            ui.setSecondaryCallInfo(false, null);
-        }
-
-        mPrimary = primary;
     }
 
     @Override
@@ -149,6 +130,25 @@
     }
 
     /**
+     * Starts a query for more contact data for the save primary and secondary calls.
+     */
+    private void startContactInfoSearch() {
+        if (mPrimary != null && mContactInfoCache != null) {
+            mContactInfoCache.findInfo(mPrimary, this);
+        } else {
+            mPrimaryContactInfo = null;
+            updatePrimaryDisplayInfo();
+        }
+
+        if (mSecondary != null && mContactInfoCache != null) {
+            mContactInfoCache.findInfo(mSecondary, this);
+        } else {
+            mSecondaryContactInfo = null;
+            updateSecondaryDisplayInfo();
+        }
+    }
+
+    /**
      * Get the highest priority call to display.
      * Goes through the calls and chooses which to return based on priority of which type of call
      * to display to the user. Callers can use the "ignore" feature to get the second best call
@@ -183,251 +183,56 @@
         return retval;
     }
 
-    public interface CallCardUi extends Ui {
-        // TODO(klp): Consider passing in the Call object directly in these methods.
-        void setVisible(boolean on);
-        void setNumber(String number);
-        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);
-        void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn);
-    }
-
-    @Override
-    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
-        if (cookie instanceof Call) {
-            final Call call = (Call) cookie;
-            if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) {
-                updateDisplayByCallerInfo(call, ci, Call.PRESENTATION_ALLOWED, true);
-            } else {
-                // If the contact doesn't exist, we can still use information from the
-                // returned caller info (geodescription, etc).
-                updateDisplayByCallerInfo(call, ci, call.getNumberPresentation(), true);
-            }
-
-            // Todo (klp): updatePhotoForCallState(call);
-        }
-    }
-
     /**
-     * Based on the given caller info, determine a suitable name, phone number and label
-     * to be passed to the CallCardUI.
-     *
-     * If the current call is a conference call, use
-     * updateDisplayForConference() instead.
-     */
-    private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation,
-            boolean isPrimary) {
-
-        // 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;
-        String displayNumber = null;
-        String label = null;
-        Uri personUri = null;
-
-        // Gather missing info unless the call is generic, in which case we wouldn't use
-        // the gathered information anyway.
-        if (info != null) {
-
-            // It appears that there is a small change in behaviour with the
-            // PhoneUtils' startGetCallerInfo whereby if we query with an
-            // empty number, we will get a valid CallerInfo object, but with
-            // fields that are all null, and the isTemporary boolean input
-            // parameter as true.
-
-            // In the past, we would see a NULL callerinfo object, but this
-            // ends up causing null pointer exceptions elsewhere down the
-            // line in other cases, so we need to make this fix instead. It
-            // appears that this was the ONLY call to PhoneUtils
-            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
-            // an unknown contact.
-
-            // Currently, infi.phoneNumber may actually be a SIP address, and
-            // if so, it might sometimes include the "sip:" prefix. That
-            // prefix isn't really useful to the user, though, so strip it off
-            // if present. (For any other URI scheme, though, leave the
-            // prefix alone.)
-            // TODO: It would be cleaner for CallerInfo to explicitly support
-            // SIP addresses instead of overloading the "phoneNumber" field.
-            // Then we could remove this hack, and instead ask the CallerInfo
-            // for a "user visible" form of the SIP address.
-            String number = info.phoneNumber;
-            if ((number != null) && number.startsWith("sip:")) {
-                number = number.substring(4);
-            }
-
-            if (TextUtils.isEmpty(info.name)) {
-                // No valid "name" in the CallerInfo, so fall back to
-                // something else.
-                // (Typically, we promote the phone number up to the "name" slot
-                // onscreen, and possibly display a descriptive string in the
-                // "number" slot.)
-                if (TextUtils.isEmpty(number)) {
-                    // No name *or* number! Display a generic "unknown" string
-                    // (or potentially some other default based on the presentation.)
-                    displayName = getPresentationString(presentation);
-                    Logger.d(this, "  ==> no name *or* number! displayName = " + displayName);
-                } else if (presentation != Call.PRESENTATION_ALLOWED) {
-                    // This case should never happen since the network should never send a phone #
-                    // AND a restricted presentation. However we leave it here in case of weird
-                    // network behavior
-                    displayName = getPresentationString(presentation);
-                    Logger.d(this, "  ==> presentation not allowed! displayName = " + displayName);
-                } else if (!TextUtils.isEmpty(info.cnapName)) {
-                    // No name, but we do have a valid CNAP name, so use that.
-                    displayName = info.cnapName;
-                    info.name = info.cnapName;
-                    displayNumber = number;
-                    Logger.d(this, "  ==> cnapName available: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
-                } else {
-                    // No name; all we have is a number. This is the typical
-                    // case when an incoming call doesn't match any contact,
-                    // or if you manually dial an outgoing number using the
-                    // dialpad.
-
-                    // Promote the phone number up to the "name" slot:
-                    displayName = number;
-
-                    // ...and use the "number" slot for a geographical description
-                    // string if available (but only for incoming calls.)
-                    if ((call != null) && (call.getState() == Call.State.INCOMING)) {
-                        // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
-                        // query to only do the geoDescription lookup in the first
-                        // place for incoming calls.
-                        displayNumber = info.geoDescription; // may be null
-                        Logger.d(this, "Geodescrption: " + info.geoDescription);
-                    }
-
-                    Logger.d(this, "  ==>  no name; falling back to number: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
-                }
-            } else {
-                // We do have a valid "name" in the CallerInfo. Display that
-                // in the "name" slot, and the phone number in the "number" slot.
-                if (presentation != Call.PRESENTATION_ALLOWED) {
-                    // This case should never happen since the network should never send a name
-                    // AND a restricted presentation. However we leave it here in case of weird
-                    // network behavior
-                    displayName = getPresentationString(presentation);
-                    Logger.d(this, "  ==> valid name, but presentation not allowed!"
-                            + " displayName = " + displayName);
-                } else {
-                    displayName = info.name;
-                    displayNumber = number;
-                    label = info.phoneLabel;
-                    Logger.d(this, "  ==>  name is present in CallerInfo: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
-                }
-            }
-            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
-            Logger.d(this, "- got personUri: '" + personUri
-                    + "', based on info.person_id: " + info.person_id);
-        } else {
-            displayName = getPresentationString(presentation);
-        }
-
-        // TODO (klp): Update secondary user call info as well.
-        if (isPrimary) {
-            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.
+     * Callback received when Contact info data query completes.
      */
     @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.");
+    public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+        Logger.d(this, "onContactInfoComplete: ", entry.name);
+        Logger.d(this, "onContactInfoComplete: ", entry.number);
+        Logger.d(this, "onContactInfoComplete: ", entry.label);
+        Logger.d(this, "onContactInfoComplete: ", entry.photo);
+
+        if (mPrimary != null && mPrimary.getCallId() == callId) {
+            mPrimaryContactInfo = entry;
+            updatePrimaryDisplayInfo();
         }
-        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);
+        if (mSecondary != null && mSecondary.getCallId() == callId) {
+            mSecondaryContactInfo = entry;
+            updateSecondaryDisplayInfo();
         }
+
     }
 
-    /**
-     * 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) {
+    private void updatePrimaryDisplayInfo() {
         final CallCardUi ui = getUi();
-        ui.setName(displayName);
-        ui.setNumber(displayNumber);
-        ui.setNumberLabel(label);
+        if (ui == null) {
+            return;
+        }
+
+        if (mPrimaryContactInfo != null) {
+            ui.setPrimary(mPrimaryContactInfo.number, mPrimaryContactInfo.name,
+                    mPrimaryContactInfo.label, mPrimaryContactInfo.photo);
+        } else {
+            // reset to nothing (like at end of call)
+            ui.setPrimary(null, null, null, null);
+        }
+
+    }
+
+    private void updateSecondaryDisplayInfo() {
+        final CallCardUi ui = getUi();
+        if (ui == null) {
+            return;
+        }
+
+        if (mSecondaryContactInfo != null) {
+            ui.setSecondary(true, mSecondaryContactInfo.number, mSecondaryContactInfo.name,
+                    mSecondaryContactInfo.label, mSecondaryContactInfo.photo);
+        } else {
+            // reset to nothing so that it starts off blank next time we use it.
+            ui.setSecondary(false, null, null, null, null);
+        }
     }
 
     public void setAudioModeProvider(AudioModeProvider audioModeProvider) {
@@ -435,13 +240,10 @@
         mAudioModeProvider.addListener(this);
     }
 
-    public String getPresentationString(int presentation) {
-        String name = mContext.getString(R.string.unknown);
-        if (presentation == Call.PRESENTATION_RESTRICTED) {
-            name = mContext.getString(R.string.private_num);
-        } else if (presentation == Call.PRESENTATION_PAYPHONE) {
-            name = mContext.getString(R.string.payphone);
-        }
-        return name;
+    public interface CallCardUi extends Ui {
+        void setVisible(boolean on);
+        void setPrimary(String number, String name, String label, Drawable photo);
+        void setSecondary(boolean show, String number, String name, String label, Drawable photo);
+        void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn);
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index 3041fa7..eb05cdb 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -221,7 +221,6 @@
             }
         }
 
-        Logger.v(this, "Found call: ", retval);
         return retval;
     }
 
diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java
index 077502b..845a6e3 100644
--- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java
+++ b/InCallUI/src/com/android/incallui/CallerInfoUtils.java
@@ -24,13 +24,12 @@
     private static final int QUERY_TOKEN = -1;
 
     /**
-     * This is called to get caller info for a call. For outgoing calls, uri should not be null
-     * because we know which contact uri the user selected to make the outgoing call. This
-     * will return a CallerInfo object immediately based off information in the call, but
+     * This is called to get caller info for a call. This will return a CallerInfo
+     * object immediately based off information in the call, but
      * more information is returned to the OnQueryCompleteListener (which contains
      * information about the phone number label, user's name, etc).
      */
-    public static CallerInfo getCallerInfoForCall(Context context, Call call, Uri uri,
+    public static CallerInfo getCallerInfoForCall(Context context, Call call,
             CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
         CallerInfo info = new CallerInfo();
         String number = call.getNumber();
@@ -42,29 +41,26 @@
         info.numberPresentation = call.getNumberPresentation();
         info.namePresentation = call.getCnapNamePresentation();
 
-        if (uri != null) {
-            // Have an URI, so pass it to startQuery
-            CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, uri, listener, call);
-        } else {
-            if (!TextUtils.isEmpty(number)) {
-                number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
-                info.phoneNumber = number;
+        // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
 
-                // For scenarios where we may receive a valid number from the network but a
-                // restricted/unavailable presentation, we do not want to perform a contact query,
-                // so just return the existing caller info.
-                if (info.numberPresentation != Call.PRESENTATION_ALLOWED) {
-                    return info;
-                } else {
-                    // Start the query with the number provided from the call.
-                    Logger.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
-                    CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call);
-                }
-            } else {
-                // The number is null or empty (Blocked caller id or empty). Just return the
-                // caller info object as is, without starting a query.
+        if (!TextUtils.isEmpty(number)) {
+            number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
+            info.phoneNumber = number;
+
+            // For scenarios where we may receive a valid number from the network but a
+            // restricted/unavailable presentation, we do not want to perform a contact query,
+            // so just return the existing caller info.
+            if (info.numberPresentation != Call.PRESENTATION_ALLOWED) {
                 return info;
+            } else {
+                // Start the query with the number provided from the call.
+                Logger.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
+                CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call);
             }
+        } else {
+            // The number is null or empty (Blocked caller id or empty). Just return the
+            // caller info object as is, without starting a query.
+            return info;
         }
 
         return info;
diff --git a/InCallUI/src/com/android/incallui/ContactInfoCache.java b/InCallUI/src/com/android/incallui/ContactInfoCache.java
new file mode 100644
index 0000000..c5a4cf5
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 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 com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.common.base.Preconditions;
+
+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.Looper;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+
+import com.android.services.telephony.common.Call;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class responsible for querying Contact Information for Call objects.
+ * Can perform asynchronous requests to the Contact Provider for information as well
+ * as respond synchronously for any data that it currently has cached from previous
+ * queries.
+ * This class always gets called from the UI thread so it does not need thread protection.
+ */
+public class ContactInfoCache implements CallerInfoAsyncQuery.OnQueryCompleteListener,
+        ContactsAsyncHelper.OnImageLoadCompleteListener {
+
+    private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+
+    private final Context mContext;
+    private final Map<Integer, SearchEntry> mInfoMap = Maps.newHashMap();
+
+    public ContactInfoCache(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Requests contact data for the Call object passed in.
+     * Returns the data through callback.  If callback is null, no response is made, however the
+     * query is still performed and cached.
+     *
+     * @param call The call to look up.
+     * @param callback The function to call back when the call is found. Can be null.
+     */
+    public void findInfo(Call call, ContactInfoCacheCallback callback) {
+        Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(call);
+
+        final SearchEntry entry;
+
+        // If the entry already exists, add callback
+        if (mInfoMap.containsKey(call.getCallId())) {
+            entry = mInfoMap.get(call.getCallId());
+
+            // If this entry is still pending, the callback will also get called when it returns.
+            if (!entry.finished) {
+                entry.addCallback(callback);
+            }
+        } else {
+            entry = new SearchEntry(call, callback);
+            mInfoMap.put(call.getCallId(), entry);
+            startQuery(entry);
+        }
+
+        // Call back with the information we have
+        callback.onContactInfoComplete(entry.call.getCallId(), entry.info);
+    }
+
+    /**
+     * Callback method for asynchronous caller information query.
+     */
+    @Override
+    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+        if (cookie instanceof Call) {
+            final Call call = (Call) cookie;
+
+            if (!mInfoMap.containsKey(call.getCallId())) {
+                return;
+            }
+
+            final SearchEntry entry = mInfoMap.get(call.getCallId());
+
+            int presentationMode = call.getNumberPresentation();
+            if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) {
+                presentationMode = Call.PRESENTATION_ALLOWED;
+            }
+
+            // start photo query
+
+            updateCallerInfo(entry, ci, presentationMode);
+        }
+    }
+
+    /**
+     * 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) {
+        Logger.d(this, "Image load complete with context: ", mContext);
+        // 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);
+
+        final Call call = (Call) cookie;
+
+        if (!mInfoMap.containsKey(call.getCallId())) {
+            Logger.e(this, "Image Load received for empty search entry.");
+            return;
+        }
+
+        final SearchEntry entry = mInfoMap.get(call.getCallId());
+
+        Logger.d(this, "setting photo for entry: ", entry);
+
+        // TODO (klp): Handle conference calls
+        if (photo != null) {
+            Logger.v(this, "direct drawable: ", photo);
+            entry.info.photo = photo;
+        } else if (photoIcon != null) {
+            Logger.v(this, "photo icon: ", photoIcon);
+            entry.info.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+        } else {
+            Logger.v(this, "unknown photo");
+            entry.info.photo = null;
+        }
+
+        sendNotification(entry);
+    }
+
+    /**
+     * Blows away the stored cache values.
+     */
+    public void clearCache() {
+        mInfoMap.clear();
+    }
+
+    /**
+     * Performs a query for caller information.
+     * Save any immediate data we get from the query. An asynchronous query may also be made
+     * for any data that we do not already have. Some queries, such as those for voicemail and
+     * emergency call information, will not perform an additional asynchronous query.
+     */
+    private void startQuery(SearchEntry entry) {
+        final CallerInfo ci = CallerInfoUtils.getCallerInfoForCall(mContext, entry.call, this);
+
+        updateCallerInfo(entry, ci, entry.call.getNumberPresentation());
+    }
+
+    private void updateCallerInfo(SearchEntry entry, CallerInfo info, int presentation) {
+        // The actual strings we're going to display onscreen:
+        String displayName;
+        String displayNumber = null;
+        String label = null;
+        Uri personUri = null;
+        Drawable photo = null;
+
+        final Call call = entry.call;
+
+        // Gather missing info unless the call is generic, in which case we wouldn't use
+        // the gathered information anyway.
+        if (info != null) {
+
+            // It appears that there is a small change in behaviour with the
+            // PhoneUtils' startGetCallerInfo whereby if we query with an
+            // empty number, we will get a valid CallerInfo object, but with
+            // fields that are all null, and the isTemporary boolean input
+            // parameter as true.
+
+            // In the past, we would see a NULL callerinfo object, but this
+            // ends up causing null pointer exceptions elsewhere down the
+            // line in other cases, so we need to make this fix instead. It
+            // appears that this was the ONLY call to PhoneUtils
+            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
+            // an unknown contact.
+
+            // Currently, infi.phoneNumber may actually be a SIP address, and
+            // if so, it might sometimes include the "sip:" prefix. That
+            // prefix isn't really useful to the user, though, so strip it off
+            // if present. (For any other URI scheme, though, leave the
+            // prefix alone.)
+            // TODO: It would be cleaner for CallerInfo to explicitly support
+            // SIP addresses instead of overloading the "phoneNumber" field.
+            // Then we could remove this hack, and instead ask the CallerInfo
+            // for a "user visible" form of the SIP address.
+            String number = info.phoneNumber;
+            if ((number != null) && number.startsWith("sip:")) {
+                number = number.substring(4);
+            }
+
+            if (TextUtils.isEmpty(info.name)) {
+                // No valid "name" in the CallerInfo, so fall back to
+                // something else.
+                // (Typically, we promote the phone number up to the "name" slot
+                // onscreen, and possibly display a descriptive string in the
+                // "number" slot.)
+                if (TextUtils.isEmpty(number)) {
+                    // No name *or* number! Display a generic "unknown" string
+                    // (or potentially some other default based on the presentation.)
+                    displayName = getPresentationString(presentation);
+                    Logger.d(this, "  ==> no name *or* number! displayName = " + displayName);
+                } else if (presentation != Call.PRESENTATION_ALLOWED) {
+                    // This case should never happen since the network should never send a phone #
+                    // AND a restricted presentation. However we leave it here in case of weird
+                    // network behavior
+                    displayName = getPresentationString(presentation);
+                    Logger.d(this, "  ==> presentation not allowed! displayName = " + displayName);
+                } else if (!TextUtils.isEmpty(info.cnapName)) {
+                    // No name, but we do have a valid CNAP name, so use that.
+                    displayName = info.cnapName;
+                    info.name = info.cnapName;
+                    displayNumber = number;
+                    Logger.d(this, "  ==> cnapName available: displayName '"
+                            + displayName + "', displayNumber '" + displayNumber + "'");
+                } else {
+                    // No name; all we have is a number. This is the typical
+                    // case when an incoming call doesn't match any contact,
+                    // or if you manually dial an outgoing number using the
+                    // dialpad.
+
+                    // Promote the phone number up to the "name" slot:
+                    displayName = number;
+
+                    // ...and use the "number" slot for a geographical description
+                    // string if available (but only for incoming calls.)
+                    if ((call != null) && (call.getState() == Call.State.INCOMING)) {
+                        // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
+                        // query to only do the geoDescription lookup in the first
+                        // place for incoming calls.
+                        displayNumber = info.geoDescription; // may be null
+                        Logger.d(this, "Geodescrption: " + info.geoDescription);
+                    }
+
+                    Logger.d(this, "  ==>  no name; falling back to number: displayName '"
+                            + displayName + "', displayNumber '" + displayNumber + "'");
+                }
+            } else {
+                // We do have a valid "name" in the CallerInfo. Display that
+                // in the "name" slot, and the phone number in the "number" slot.
+                if (presentation != Call.PRESENTATION_ALLOWED) {
+                    // This case should never happen since the network should never send a name
+                    // AND a restricted presentation. However we leave it here in case of weird
+                    // network behavior
+                    displayName = getPresentationString(presentation);
+                    Logger.d(this, "  ==> valid name, but presentation not allowed!"
+                            + " displayName = " + displayName);
+                } else {
+                    displayName = info.name;
+                    displayNumber = number;
+                    label = info.phoneLabel;
+                    Logger.d(this, "  ==>  name is present in CallerInfo: displayName '"
+                            + displayName + "', displayNumber '" + displayNumber + "'");
+                }
+            }
+            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
+            Logger.d(this, "- got personUri: '" + personUri
+                    + "', based on info.person_id: " + info.person_id);
+        } else {
+            displayName = getPresentationString(presentation);
+        }
+
+        // This will only be true for emergency numbers
+        if (info.photoResource != 0) {
+            photo = mContext.getResources().getDrawable(info.photoResource);
+        } else if (info.isCachedPhotoCurrent) {
+            if (info.cachedPhoto != null) {
+                photo = info.cachedPhoto;
+            } else {
+                photo = mContext.getResources().getDrawable(R.drawable.picture_unknown);
+            }
+        } else {
+            if (personUri == null) {
+                Logger.v(this, "personUri is null. Just use unknown picture.");
+                photo = mContext.getResources().getDrawable(R.drawable.picture_unknown);
+            } else {
+                Logger.d(this, "startObtainPhotoAsync");
+                // 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, entry.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);
+            }
+        }
+
+        final ContactCacheEntry cce = entry.info;
+        cce.name = displayName;
+        cce.number = displayNumber;
+        cce.label = label;
+        cce.photo = photo;
+
+        sendNotification(entry);
+    }
+
+    /**
+     * Sends the updated information to call the callbacks for the entry.
+     */
+    private void sendNotification(SearchEntry entry) {
+        for (int i = 0; i < entry.callbacks.size(); i++) {
+            entry.callbacks.get(i).onContactInfoComplete(entry.call.getCallId(), entry.info);
+        }
+    }
+
+    /**
+     * Gets name strings based on some special presentation modes.
+     */
+    private String getPresentationString(int presentation) {
+        String name = mContext.getString(R.string.unknown);
+        if (presentation == Call.PRESENTATION_RESTRICTED) {
+            name = mContext.getString(R.string.private_num);
+        } else if (presentation == Call.PRESENTATION_PAYPHONE) {
+            name = mContext.getString(R.string.payphone);
+        }
+        return name;
+    }
+
+    /**
+     * Callback interface for the contact query.
+     */
+    public interface ContactInfoCacheCallback {
+        public void onContactInfoComplete(int callId, ContactCacheEntry entry);
+    }
+
+    public static class ContactCacheEntry {
+        public String name;
+        public String number;
+        public String label;
+        public Drawable photo;
+    }
+
+    private static class SearchEntry {
+        public Call call;
+        public boolean finished;
+        public final ContactCacheEntry info;
+        public final List<ContactInfoCacheCallback> callbacks = Lists.newArrayList();
+
+        public SearchEntry(Call call, ContactInfoCacheCallback callback) {
+            this.call = call;
+
+            info = new ContactCacheEntry();
+            finished = false;
+            callbacks.add(callback);
+        }
+
+        public void addCallback(ContactInfoCacheCallback cb) {
+            if (!callbacks.contains(cb)) {
+                callbacks.add(cb);
+            }
+        }
+
+        public void finish() {
+            callbacks.clear();
+            finished = true;
+        }
+    }
+}
diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
index c9a3317..305486f 100644
--- a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
+++ b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java
@@ -38,9 +38,6 @@
  */
 public class ContactsAsyncHelper {
 
-    private static final boolean DBG = false;
-    private static final String LOG_TAG = "ContactsAsyncHelper";
-
     /**
      * Interface for a WorkerHandler result return.
      */
@@ -71,10 +68,8 @@
             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");
-                        }
+                        Logger.d(this, "Notifying listener: " + args.listener.toString() +
+                                " image: " + args.uri + " completed");
                         args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
                                 args.cookie);
                     }
@@ -197,7 +192,7 @@
                             inputStream = Contacts.openContactPhotoInputStream(
                                     args.context.getContentResolver(), args.uri, true);
                         } catch (Exception e) {
-                            Log.e(LOG_TAG, "Error opening photo input stream", e);
+                            Logger.e(this, "Error opening photo input stream", e);
                         }
 
                         if (inputStream != null) {
@@ -208,25 +203,21 @@
                             // 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);
-                            }
+                            Logger.d(ContactsAsyncHelper.this, "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.");
-                            }
+                            Logger.d(ContactsAsyncHelper.this, "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);
+                                Logger.e(this, "Unable to close input stream.", e);
                             }
                         }
                     }
@@ -264,7 +255,7 @@
                 // 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.");
+                    Logger.w(this, "Photo icon's width or height become 0.");
                     return null;
                 }
 
@@ -307,7 +298,7 @@
         // 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");
+            Logger.wtf("startObjectPhotoAsync", "Uri is missing");
             return;
         }
 
@@ -326,7 +317,7 @@
         msg.arg1 = EVENT_LOAD_IMAGE;
         msg.obj = args;
 
-        if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
+        Logger.d("startObjectPhotoAsync", "Begin loading image: " + args.uri +
                 ", displaying default image for now.");
 
         // notify the thread to begin working
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index 8ae37bd..e1a9796 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -34,6 +34,7 @@
     private CallCardFragment mCallCardFragment;
     private AnswerFragment mAnswerFragment;
     private DialpadFragment mDialpadFragment;
+    private boolean mIsForegroundActivity;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -62,6 +63,9 @@
     protected void onResume() {
         Logger.d(this, "onResume()...");
         super.onResume();
+
+        mIsForegroundActivity = true;
+        InCallPresenter.getInstance().onUiShowing(true);
     }
 
     // onPause is guaranteed to be called when the InCallActivity goes
@@ -70,6 +74,9 @@
     protected void onPause() {
         Logger.d(this, "onPause()...");
         super.onPause();
+
+        mIsForegroundActivity = false;
+        InCallPresenter.getInstance().onUiShowing(false);
     }
 
     @Override
@@ -85,6 +92,13 @@
     }
 
     /**
+     * Returns true when theActivity is in foreground (between onResume and onPause).
+     */
+    /* package */ boolean isForegroundActivity() {
+        return mIsForegroundActivity;
+    }
+
+    /**
      * Dismisses the in-call screen.
      *
      * We never *really* finish() the InCallActivity, since we don't want to get destroyed and then
@@ -273,6 +287,8 @@
                 mainPresenter.getAudioModeProvider());
         mCallCardFragment.getPresenter().setAudioModeProvider(
                 mainPresenter.getAudioModeProvider());
+        mCallCardFragment.getPresenter().setContactInfoCache(
+                mainPresenter.getContactInfoCache());
 
         mainPresenter.addListener(mCallButtonFragment.getPresenter());
         mainPresenter.addListener(mCallCardFragment.getPresenter());
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index c56d980..3ae4d3a 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -43,6 +43,7 @@
 
     private AudioModeProvider mAudioModeProvider;
     private StatusBarNotifier mStatusBarNotifier;
+    private ContactInfoCache mContactInfoCache;
     private Context mContext;
     private CallList mCallList;
     private InCallActivity mInCallActivity;
@@ -63,7 +64,9 @@
         mCallList = callList;
         mCallList.addListener(this);
 
-        mStatusBarNotifier = new StatusBarNotifier(context);
+        mContactInfoCache = new ContactInfoCache(context);
+
+        mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache, mCallList);
         addListener(mStatusBarNotifier);
 
         mAudioModeProvider = audioModeProvider;
@@ -148,6 +151,10 @@
         return mAudioModeProvider;
     }
 
+    public ContactInfoCache getContactInfoCache() {
+        return mContactInfoCache;
+    }
+
     /**
      * Hangs up any active or outgoing calls.
      */
@@ -163,6 +170,23 @@
     }
 
     /**
+     * Returns true if the incall app is the foreground application.
+     */
+    public boolean isShowingInCallUi() {
+        return (mInCallActivity != null &&
+                mInCallActivity.isForegroundActivity());
+    }
+
+    /**
+     * Called when the activity goes out of the foreground.
+     */
+    public void onUiShowing(boolean showing) {
+        // We need to update the notification bar when we leave the UI because that
+        // could trigger it to show again.
+        mStatusBarNotifier.updateNotification(mInCallState, mCallList);
+    }
+
+    /**
      * When the state of in-call changes, this is the first method to get called. It determines if
      * the UI needs to be started or finished depending on the new state and does it.
      */
@@ -215,7 +239,7 @@
 
 
         if (startStartupSequence) {
-            mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState);
+            mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
         } else if (showCallUi) {
             showInCall();
         } else if (newState == InCallState.HIDDEN) {
@@ -227,6 +251,10 @@
                 mInCallActivity = null;
 
                 temp.finish();
+
+                // blow away stale contact info so that we get fresh data on
+                // the next set of calls
+                mContactInfoCache.clearCache();
             }
         }
 
diff --git a/InCallUI/src/com/android/incallui/Logger.java b/InCallUI/src/com/android/incallui/Logger.java
index e7cbe20..10433be 100644
--- a/InCallUI/src/com/android/incallui/Logger.java
+++ b/InCallUI/src/com/android/incallui/Logger.java
@@ -53,6 +53,12 @@
         }
     }
 
+    public static void v(Object obj, String str1, Object str2) {
+        if (VERBOSE) {
+            Log.d(TAG, getPrefix(obj) + str1 + str2);
+        }
+    }
+
     public static void e(String tag, String msg, Exception e) {
         Log.e(TAG, tag + msg, e);
     }
@@ -61,12 +67,6 @@
         Log.e(TAG, tag + msg);
     }
 
-    public static void v(Object obj, String str1, Object str2) {
-        if (VERBOSE) {
-            Log.d(TAG, getPrefix(obj) + str1 + str2);
-        }
-    }
-
     public static void e(Object obj, String msg, Exception e) {
         Log.e(TAG, getPrefix(obj) + msg, e);
     }
@@ -83,6 +83,10 @@
         Log.i(TAG, getPrefix(obj) + msg);
     }
 
+    public static void w(Object obj, String msg) {
+        Log.w(TAG, getPrefix(obj) + msg);
+    }
+
     public static void wtf(Object obj, String msg) {
         Log.wtf(TAG, getPrefix(obj) + msg);
     }
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
index 857b6ce..e2dc42c 100644
--- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java
+++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
@@ -23,7 +23,13 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
 import com.android.incallui.InCallApp.NotificationBroadcastReceiver;
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.services.telephony.common.Call;
@@ -31,18 +37,29 @@
 /**
  * This class adds Notifications to the status bar for the in-call experience.
  */
-public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
+public class StatusBarNotifier implements InCallPresenter.InCallStateListener,
+        ContactInfoCacheCallback {
     // notification types
     private static final int IN_CALL_NOTIFICATION = 1;
 
     private final Context mContext;
+    private final ContactInfoCache mContactInfoCache;
+    private final CallList mCallList;
     private final NotificationManager mNotificationManager;
+    private boolean mIsShowingNotification = false;
     private InCallState mInCallState = InCallState.HIDDEN;
+    private int mSavedIcon = 0;
+    private int mSavedContent = 0;
+    private Bitmap mSavedLargeIcon;
+    private String mSavedContentTitle;
 
-    public StatusBarNotifier(Context context) {
+    public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache,
+            CallList callList) {
         Preconditions.checkNotNull(context);
 
         mContext = context;
+        mContactInfoCache = contactInfoCache;
+        mCallList = callList;
         mNotificationManager =
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
     }
@@ -52,7 +69,15 @@
      */
     @Override
     public void onStateChange(InCallState state, CallList callList) {
-        updateInCallNotification(state);
+        updateNotification(state, callList);
+    }
+
+    /**
+     * Called after the Contact Info query has finished.
+     */
+    @Override
+    public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+        updateNotification(mInCallState, mCallList);
     }
 
     /**
@@ -63,10 +88,10 @@
      * This method will never actually launch the incoming-call UI.
      * (Use updateNotificationAndLaunchIncomingCallUi() for that.)
      */
-    private void updateInCallNotification(InCallState state) {
+    public void updateNotification(InCallState state, CallList callList) {
         // allowFullScreenIntent=false means *don't* allow the incoming
         // call UI to be launched.
-        updateInCallNotification(false, state);
+        updateInCallNotification(false, state, callList);
     }
 
     /**
@@ -100,10 +125,10 @@
      *
      * @see #updateInCallNotification(boolean)
      */
-    public void updateNotificationAndLaunchIncomingCallUi(InCallState state) {
+    public void updateNotificationAndLaunchIncomingCallUi(InCallState state, CallList callList) {
         // Set allowFullScreenIntent=true to indicate that we *should*
         // launch the incoming call UI if necessary.
-        updateInCallNotification(true, state);
+        updateInCallNotification(true, state, callList);
     }
 
 
@@ -114,6 +139,8 @@
     private void cancelInCall() {
         Logger.d(this, "cancelInCall()...");
         mNotificationManager.cancel(IN_CALL_NOTIFICATION);
+
+        mIsShowingNotification = false;
     }
 
     /**
@@ -129,37 +156,76 @@
      *   Watch out: This should be set to true *only* when directly
      *   handling a new incoming call for the first time.
      */
-    private void updateInCallNotification(boolean allowFullScreenIntent, InCallState state) {
+    private void updateInCallNotification(final boolean allowFullScreenIntent,
+            final InCallState state, CallList callList) {
         Logger.d(this, "updateInCallNotification(allowFullScreenIntent = "
                      + allowFullScreenIntent + ")...");
 
-        // First, we dont need to continue issuing new notifications if the state hasn't
-        // changed from the last time we did this.
-        if (mInCallState == state) {
-            return;
-        }
-        mInCallState = state;
-
-        if (!state.isConnectingOrConnected()) {
+        if (shouldSuppressNotification(state, callList)) {
             cancelInCall();
             return;
         }
 
-        final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
-        final Notification.Builder builder = getNotificationBuilder();
-        builder.setContentIntent(inCallPendingIntent);
+        final Call call = getCallToShow(callList);
+        if (call == null) {
+            Logger.wtf(this, "No call for the notification!");
+        }
+
+        // we make a call to the contact info cache to query for supplemental data to what the
+        // call provides.  This includes the contact name and photo.
+        // This callback will always get called immediately and synchronously with whatever data
+        // it has available, and may make a subsequent call later (same thread) if it had to
+        // call into the contacts provider for more data.
+        mContactInfoCache.findInfo(call, new ContactInfoCacheCallback() {
+            @Override
+            public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+                buildAndSendNotification(state, call, entry, allowFullScreenIntent);
+            }
+        });
+
+    }
+
+    /**
+     * Sets up the main Ui for the notification
+     */
+    private void buildAndSendNotification(InCallState state, Call call,
+            ContactCacheEntry contactInfo, boolean allowFullScreenIntent) {
+
+        final int iconResId = getIconToDisplay(call);
+        final Bitmap largeIcon = getLargeIconToDisplay(contactInfo);
+        final int contentResId = getContentString(call);
+        final String contentTitle = getContentTitle(contactInfo);
+
+        // If we checked and found that nothing is different, dont issue another notification.
+        if (!checkForChangeAndSaveData(iconResId, contentResId, largeIcon, contentTitle, state,
+                allowFullScreenIntent)) {
+            return;
+        }
 
         /*
-         * Set up the Intents that will get fired when the user interacts with the notificaiton.
+         * Nothing more to check...build and send it.
          */
+        final Notification.Builder builder = getNotificationBuilder();
+
+        // Set up the main intent to send the user to the in-call screen
+        final PendingIntent inCallPendingIntent = createLaunchPendingIntent();
+        builder.setContentIntent(inCallPendingIntent);
+
+        // Set the intent as a full screen intent as well if requested
         if (allowFullScreenIntent) {
             configureFullScreenIntent(builder, inCallPendingIntent);
         }
 
-        /*
-         * Set up notification Ui.
-         */
-        setUpNotification(builder, state);
+        // set the content
+        builder.setContentText(mContext.getString(contentResId));
+        builder.setSmallIcon(iconResId);
+        builder.setContentTitle(contentTitle);
+        builder.setLargeIcon(largeIcon);
+
+        // Add special Content for calls that are ongoing
+        if (InCallState.INCALL == state || InCallState.OUTGOING == state) {
+            addHangupAction(builder);
+        }
 
         /*
          * Fire off the notification
@@ -167,24 +233,129 @@
         Notification notification = builder.build();
         Logger.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification);
         mNotificationManager.notify(IN_CALL_NOTIFICATION, notification);
+        mIsShowingNotification = true;
     }
 
     /**
-     * Sets up the main Ui for the notification
+     * Checks the new notification data and compares it against any notification that we
+     * are already displaying. If the data is exactly the same, we return false so that
+     * we do not issue a new notification for the exact same data.
      */
-    private void setUpNotification(Notification.Builder builder, InCallState state) {
+    private boolean checkForChangeAndSaveData(int icon, int content, Bitmap largeIcon,
+            String contentTitle, InCallState state, boolean showFullScreenIntent) {
 
-        // Add special Content for calls that are ongoing
-        if (InCallState.INCALL == state || InCallState.OUTGOING == state) {
-            addActiveCallIntents(builder);
+        // The two are different:
+        // if new title is not null, it should be different from saved version OR
+        // if new title is null, the saved version should not be null
+        final boolean contentTitleChanged =
+                (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) ||
+                (contentTitle == null && mSavedContentTitle != null);
+
+        // any change means we are definitely updating
+        boolean retval = (mSavedIcon != icon) || (mSavedContent != content) ||
+                (mInCallState != state) || (mSavedLargeIcon != largeIcon) ||
+                contentTitleChanged;
+
+        // A full screen intent means that we have been asked to interrupt an activity,
+        // so we definitely want to show it.
+        if (showFullScreenIntent) {
+            Logger.d(this, "Forcing full screen intent");
+            retval = true;
         }
 
-        // set the content
-        builder.setContentText(mContext.getString(R.string.notification_ongoing_call));
-        builder.setSmallIcon(R.drawable.stat_sys_phone_call);
+        // If we aren't showing a notification right now, definitely start showing one.
+        if (!mIsShowingNotification) {
+            Logger.d(this, "Showing notification for first time.");
+            retval = true;
+        }
+
+        mSavedIcon = icon;
+        mSavedContent = content;
+        mInCallState = state;
+        mSavedLargeIcon = largeIcon;
+        mSavedContentTitle = contentTitle;
+
+        if (retval) {
+            Logger.d(this, "Data changed.  Showing notification");
+        }
+
+        return retval;
     }
 
-    private void addActiveCallIntents(Notification.Builder builder) {
+    /**
+     * Returns the main string to use in the notification.
+     */
+    private String getContentTitle(ContactCacheEntry contactInfo) {
+        if (TextUtils.isEmpty(contactInfo.name)) {
+            return contactInfo.number;
+        }
+
+        return contactInfo.name;
+    }
+
+    /**
+     * Gets a large icon from the contact info object to display in the notification.
+     */
+    private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo) {
+        if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+            return ((BitmapDrawable) contactInfo.photo).getBitmap();
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the appropriate icon res Id to display based on the call for which
+     * we want to display information.
+     */
+    private int getIconToDisplay(Call call) {
+        // Even if both lines are in use, we only show a single item in
+        // the expanded Notifications UI.  It's labeled "Ongoing call"
+        // (or "On hold" if there's only one call, and it's on hold.)
+        // Also, we don't have room to display caller-id info from two
+        // different calls.  So if both lines are in use, display info
+        // from the foreground call.  And if there's a ringing call,
+        // display that regardless of the state of the other calls.
+        if (call.getState() == Call.State.ONHOLD) {
+            return R.drawable.stat_sys_phone_call_on_hold;
+        }
+        return R.drawable.stat_sys_phone_call;
+    }
+
+    /**
+     * Returns the message to use with the notificaiton.
+     */
+    private int getContentString(Call call) {
+        int resId = R.string.notification_ongoing_call;
+
+        if (call.getState() == Call.State.INCOMING) {
+            resId = R.string.notification_incoming_call;
+
+        } else if (call.getState() == Call.State.ONHOLD) {
+            resId = R.string.notification_on_hold;
+
+        } else if (call.getState() == Call.State.DIALING) {
+            resId = R.string.notification_dialing;
+        }
+
+        return resId;
+    }
+
+    /**
+     * Gets the most relevant call to display in the notification.
+     */
+    private Call getCallToShow(CallList callList) {
+        Call call = callList.getIncomingCall();
+        if (call == null) {
+            call = callList.getOutgoingCall();
+        }
+        if (call == null) {
+            call = callList.getActiveOrBackgroundCall();
+        }
+        return call;
+    }
+
+    private void addHangupAction(Notification.Builder builder) {
         Logger.i(this, "Will show \"hang-up\" action in the ongoing active call Notification");
 
         // TODO: use better asset.
@@ -246,6 +417,53 @@
         return builder;
     }
 
+    /**
+     * Returns true if notification should not be shown in the current state.
+     */
+    private boolean shouldSuppressNotification(InCallState state, CallList callList) {
+        // Suppress the in-call notification if the InCallScreen is the
+        // foreground activity, since it's already obvious that you're on a
+        // call.  (The status bar icon is needed only if you navigate *away*
+        // from the in-call UI.)
+        boolean shouldSuppress = InCallPresenter.getInstance().isShowingInCallUi();
+
+        // Suppress if the call is not active.
+        if (!state.isConnectingOrConnected()) {
+            shouldSuppress = true;
+        }
+
+        // We can still be in the INCALL state when a call is disconnected (in order to show
+        // the "Call ended" screen.  So check that we have an active connection too.
+        final Call call = getCallToShow(callList);
+        if (call == null) {
+            shouldSuppress = true;
+        }
+
+        // If there's an incoming ringing call: always show the
+        // notification, since the in-call notification is what actually
+        // launches the incoming call UI in the first place (see
+        // notification.fullScreenIntent below.)  This makes sure that we'll
+        // correctly handle the case where a new incoming call comes in but
+        // the InCallScreen is already in the foreground.
+        if (state.isIncoming()) {
+            shouldSuppress = false;
+        }
+
+        // JANK fix:
+        // This class will issue a notification when user makes an outgoing call.
+        // However, since we suppress the notification when the user is in the in-call screen,
+        // that results is us showing it for a split second, until the in-call screen comes up.
+        // It looks ugly.
+        //
+        // The solution is to ignore the change from HIDDEN to OUTGOING since in that particular
+        // case, we know we'll get called to update again when the UI finally starts.
+        if (InCallState.OUTGOING == state && InCallState.HIDDEN == mInCallState) {
+            shouldSuppress = true;
+        }
+
+        return shouldSuppress;
+    }
+
     private PendingIntent createLaunchPendingIntent() {
 
         final Intent intent = new Intent(Intent.ACTION_MAIN, null);