Major fixes for in call to work with reverse number lookup.

- Separated caller info data into CallIdentification and switch callbacks to use
it where call state is un-necessary.
- Changed mCallList.update() method to be onIncoming().
- Catch all exceptions from service methods so errors do not vanish.
- Fixed bind failure cases which led to DeadObjectException.
- Changed local contact lookup to only occur for incoming calls.
- Fixed CallCardPresenter to start contact search upon isntantiation instead of
waiting for next call update.
- Convert ContactInfoCache to singleton to avoid race condition where it's not
initialized.
- Handle cases where primary call may be null when we find a contact.
- Fixed race conditions in CallButtonPresenter where audio mode is being set
before ui is ready.
- Fixed race condition in AnswerPresenter where state change was being called
before ui is ready.
- Changes to CallCardPresenter to support lookup for conference calls.

Bug: 10413515
Bug: 10390984

Change-Id: I9fc4f2f35e8f5aad33c301b3c5c93132634cb63c
diff --git a/InCallUI/res/layout/answer_fragment.xml b/InCallUI/res/layout/answer_fragment.xml
index f6b1320..5f8c561 100644
--- a/InCallUI/res/layout/answer_fragment.xml
+++ b/InCallUI/res/layout/answer_fragment.xml
@@ -30,6 +30,7 @@
         android:gravity="center"
         android:layout_gravity="bottom|center_horizontal"
         android:background="@android:color/black"
+        android:visibility="gone"
 
         dc:targetDrawables="@array/incoming_call_widget_2way_targets"
         dc:targetDescriptions="@array/incoming_call_widget_2way_target_descriptions"
diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java
index 077eaac..40462ce 100644
--- a/InCallUI/src/com/android/incallui/AnswerFragment.java
+++ b/InCallUI/src/com/android/incallui/AnswerFragment.java
@@ -16,8 +16,6 @@
 
 package com.android.incallui;
 
-import com.google.common.base.Preconditions;
-
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.os.Bundle;
@@ -28,6 +26,8 @@
 import android.widget.ArrayAdapter;
 import android.widget.ListView;
 
+import com.google.common.base.Preconditions;
+
 import java.util.ArrayList;
 
 /**
@@ -39,10 +39,9 @@
     /**
      * The popup showing the list of canned responses.
      *
-     * This is an AlertDialog containing a ListView showing the possible
-     * choices.  This may be null if the InCallScreen hasn't ever called
-     * showRespondViaSmsPopup() yet, or if the popup was visible once but
-     * then got dismissed.
+     * This is an AlertDialog containing a ListView showing the possible choices.  This may be null
+     * if the InCallScreen hasn't ever called showRespondViaSmsPopup() yet, or if the popup was
+     * visible once but then got dismissed.
      */
     private Dialog mCannedResponsePopup = null;
 
@@ -101,9 +100,8 @@
         lv.setAdapter(mTextResponsesAdapter);
         lv.setOnItemClickListener(new RespondViaSmsItemClickListener());
 
-        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
-                .setCancelable(true)
-                .setView(lv);
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setCancelable(
+                true).setView(lv);
         mCannedResponsePopup = builder.create();
         mCannedResponsePopup.show();
     }
@@ -111,9 +109,8 @@
     /**
      * Dismiss currently visible popups.
      *
-     * This is safe to call even if the popup is already dismissed, and
-     * even if you never called showRespondViaSmsPopup() in the first
-     * place.
+     * This is safe to call even if the popup is already dismissed, and even if you never called
+     * showRespondViaSmsPopup() in the first place.
      */
     @Override
     public void dismissPopup() {
@@ -127,9 +124,7 @@
     public void configureMessageDialogue(ArrayList<String> textResponses) {
         textResponses.add(getResources().getString(R.string.respond_via_sms_custom_message));
         mTextResponsesAdapter = new ArrayAdapter<String>(getActivity(),
-                                         android.R.layout.simple_list_item_1,
-                                         android.R.id.text1,
-                                         textResponses);
+                android.R.layout.simple_list_item_1, android.R.id.text1, textResponses);
     }
 
     @Override
@@ -151,14 +146,14 @@
      * OnItemClickListener for the "Respond via SMS" popup.
      */
     public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
+
         /**
          * Handles the user selecting an item from the popup.
          */
         @Override
         public void onItemClick(AdapterView<?> parent,  // The ListView
-                                View view,  // The TextView that was clicked
-                                int position,
-                                long id) {
+                View view,  // The TextView that was clicked
+                int position, long id) {
             Log.d(this, "RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
             final String message = (String) parent.getItemAtPosition(position);
             Log.v(this, "- message: '" + message + "'");
diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java
index 78ee0f1..1c970d7 100644
--- a/InCallUI/src/com/android/incallui/AnswerPresenter.java
+++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java
@@ -16,8 +16,6 @@
 
 package com.android.incallui;
 
-import com.android.incallui.InCallPresenter.InCallState;
-import com.android.incallui.InCallPresenter.InCallStateListener;
 import com.android.services.telephony.common.Call;
 
 import java.util.ArrayList;
@@ -26,53 +24,94 @@
  * Presenter for the Incoming call widget.
  */
 public class AnswerPresenter extends Presenter<AnswerPresenter.AnswerUi>
-        implements InCallStateListener {
+        implements CallList.CallUpdateListener, CallList.Listener {
 
-    private Call mCall;
-    private ArrayList<String> mTextResponses;
+    private static final String TAG = AnswerPresenter.class.getSimpleName();
+
+    private int mCallId = Call.INVALID_CALL_ID;
 
     @Override
     public void onUiReady(AnswerUi ui) {
         super.onUiReady(ui);
+
+        final CallList calls = CallList.getInstance();
+        final Call call = calls.getIncomingCall();
+        // TODO: change so that answer presenter never starts up if it's not incoming.
+        if (call != null) {
+            processIncomingCall(call);
+
+            // Listen for call updates for the current call.
+            calls.addCallUpdateListener(mCallId, this);
+
+            // Listen for incoming calls.
+            calls.addListener(this);
+        }
     }
 
     @Override
-    public void onStateChange(InCallState state, CallList callList) {
-        if (state == InCallState.INCOMING) {
-            getUi().showAnswerUi(true);
-            mCall = callList.getIncomingCall();
-            mTextResponses = callList.getTextResponses(mCall);
-            if (mTextResponses != null) {
-                getUi().showTextButton(true);
-                getUi().configureMessageDialogue(mTextResponses);
-            } else {
-                getUi().showTextButton(false);
+    public void onCallListChange(CallList callList) {
+        // no-op
+    }
+
+    @Override
+    public void onIncomingCall(Call call) {
+        // TODO: Ui is being destroyed when the fragment detaches.  Need clean up step to stop
+        // getting updates here.
+        if (getUi() != null) {
+            if (call.getCallId() != mCallId) {
+                // A new call is coming in.
+                processIncomingCall(call);
             }
-            Log.d(this, "Showing incoming with: " + mCall);
+        }
+    }
+
+    private void processIncomingCall(Call call) {
+        mCallId = call.getCallId();
+        Log.d(TAG, "Showing incoming for call id: " + mCallId);
+        final ArrayList<String> textMsgs = CallList.getInstance().getTextResponses(
+                call.getCallId());
+        getUi().showAnswerUi(true);
+
+        if (textMsgs != null) {
+            getUi().showTextButton(true);
+            getUi().configureMessageDialogue(textMsgs);
         } else {
+            getUi().showTextButton(false);
+        }
+    }
+
+
+    @Override
+    public void onCallStateChanged(Call call) {
+        Log.d(TAG, "onCallStateChange() " + call);
+        if (call.getState() != Call.State.INCOMING) {
+            // Stop listening for updates.
+            CallList.getInstance().removeCallUpdateListener(mCallId, this);
+            CallList.getInstance().removeListener(this);
+
             getUi().showAnswerUi(false);
-            mCall = null;
+            mCallId = Call.INVALID_CALL_ID;
         }
     }
 
     public void onAnswer() {
-        if (mCall == null) {
+        if (mCallId == Call.INVALID_CALL_ID) {
             return;
         }
 
-        Log.d(this, "onAnswer " + mCall.getCallId());
+        Log.d(this, "onAnswer " + mCallId);
 
-        CallCommandClient.getInstance().answerCall(mCall.getCallId());
+        CallCommandClient.getInstance().answerCall(mCallId);
     }
 
     public void onDecline() {
-        if (mCall == null) {
+        if (mCallId == Call.INVALID_CALL_ID) {
             return;
         }
 
-        Log.d(this, "onDecline " + mCall.getCallId());
+        Log.d(this, "onDecline " + mCallId);
 
-        CallCommandClient.getInstance().rejectCall(mCall.getCallId(), false, null);
+        CallCommandClient.getInstance().rejectCall(mCallId, false, null);
     }
 
     public void onText() {
@@ -82,7 +121,7 @@
 
     public void rejectCallWithMessage(String message) {
         Log.d(this, "sendTextToDefaultActivity()...");
-        CallCommandClient.getInstance().rejectCall(mCall.getCallId(), true, message);
+        CallCommandClient.getInstance().rejectCall(mCallId, true, message);
         getUi().dismissPopup();
     }
 
diff --git a/InCallUI/src/com/android/incallui/BaseFragment.java b/InCallUI/src/com/android/incallui/BaseFragment.java
index 0f3d6b4..a348ce4 100644
--- a/InCallUI/src/com/android/incallui/BaseFragment.java
+++ b/InCallUI/src/com/android/incallui/BaseFragment.java
@@ -47,8 +47,8 @@
     }
 
     @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
         mPresenter.onUiReady(getUi());
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index 3a2d719..9db383f 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -39,6 +39,7 @@
     @Override
     public void onUiReady(CallButtonUi ui) {
         super.onUiReady(ui);
+
         if (mAudioModeProvider != null) {
             mAudioModeProvider.addListener(this);
         }
@@ -66,12 +67,16 @@
 
     @Override
     public void onAudioMode(int mode) {
-        getUi().setAudio(mode);
+        if (getUi() != null) {
+            getUi().setAudio(mode);
+        }
     }
 
     @Override
     public void onSupportedAudioMode(int mask) {
-        getUi().setSupportedAudio(mask);
+        if (getUi() != null) {
+            getUi().setSupportedAudio(mask);
+        }
     }
 
     public int getAudioMode() {
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index b77dc4c..d9712d1 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -42,7 +42,7 @@
     // Primary caller info
     private TextView mPhoneNumber;
     private TextView mNumberLabel;
-    private TextView mName;
+    private TextView mPrimaryName;
     private TextView mCallStateLabel;
     private ImageView mPhoto;
     private TextView mElapsedTime;
@@ -72,7 +72,10 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        getPresenter().init(getActivity(), ServiceFactory.newPhoneNumberService(getActivity()));
+        final CallList calls = CallList.getInstance();
+        final Call call = calls.getFirstCall();
+        getPresenter().init(getActivity(), ServiceFactory.newPhoneNumberService(getActivity()),
+                call);
     }
 
     @Override
@@ -90,7 +93,7 @@
         super.onViewCreated(view, savedInstanceState);
 
         mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber);
-        mName = (TextView) view.findViewById(R.id.name);
+        mPrimaryName = (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);
@@ -117,18 +120,64 @@
     }
 
     @Override
-    public void setName(String name) {
-        mName.setText(name);
+    public void setPrimaryName(String name, boolean nameIsNumber) {
+        if (TextUtils.isEmpty(name)) {
+            mPrimaryName.setText("");
+        } else {
+            mPrimaryName.setText(name);
+
+            // Set direction of the name field
+            int nameDirection = View.TEXT_DIRECTION_INHERIT;
+            if (nameIsNumber) {
+                nameDirection = View.TEXT_DIRECTION_LTR;
+            }
+            mPrimaryName.setTextDirection(nameDirection);
+        }
     }
 
     @Override
-    public void setImage(Bitmap image) {
+    public void setPrimaryImage(Bitmap image) {
         if (image != null) {
             setDrawableToImageView(mPhoto, new BitmapDrawable(getResources(), image));
         }
     }
 
     @Override
+    public void setPrimaryPhoneNumber(String number) {
+        // Set the number
+        if (TextUtils.isEmpty(number)) {
+            mPhoneNumber.setText("");
+            mPhoneNumber.setVisibility(View.GONE);
+        } else {
+            mPhoneNumber.setText(number);
+            mPhoneNumber.setVisibility(View.VISIBLE);
+            mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
+        }
+    }
+
+    @Override
+    public void setPrimaryLabel(String label) {
+        if (!TextUtils.isEmpty(label)) {
+            mNumberLabel.setText(label);
+            mNumberLabel.setVisibility(View.VISIBLE);
+        } else {
+            mNumberLabel.setVisibility(View.GONE);
+        }
+
+    }
+
+    @Override
+    public void setPrimaryGateway(String gatewayLabel, String gatewayNumber) {
+        if (!TextUtils.isEmpty(gatewayLabel) && !TextUtils.isEmpty(gatewayNumber)) {
+            mProviderLabel.setText(gatewayLabel);
+            mProviderNumber.setText(gatewayNumber);
+            mProviderInfo.setVisibility(View.VISIBLE);
+        } else {
+            mProviderInfo.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
     public void setPrimary(String number, String name, boolean nameIsNumber, String label,
             Drawable photo, boolean isConference, String gatewayLabel, String gatewayNumber) {
         Log.d(this, "Setting primary call [" + gatewayLabel + "][" + gatewayNumber + "]");
@@ -138,47 +187,16 @@
             photo = getView().getResources().getDrawable(R.drawable.picture_conference);
         }
 
-        // Set the number
-        if (TextUtils.isEmpty(number)) {
-            mPhoneNumber.setText("");
-            mPhoneNumber.setVisibility(View.GONE);
-        } else {
-            mPhoneNumber.setText(number);
-            mPhoneNumber.setVisibility(View.VISIBLE);
-            mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
-        }
+        setPrimaryPhoneNumber(number);
 
         // Set any gateway information
-        if (!TextUtils.isEmpty(gatewayLabel) && !TextUtils.isEmpty(gatewayNumber)) {
-            mProviderLabel.setText(gatewayLabel);
-            mProviderNumber.setText(gatewayNumber);
-            mProviderInfo.setVisibility(View.VISIBLE);
-        } else {
-            mProviderInfo.setVisibility(View.GONE);
-        }
-
-        // Set direction of the name field
+        setPrimaryGateway(gatewayLabel, gatewayNumber);
 
         // set the name field.
-        if (TextUtils.isEmpty(name)) {
-            mName.setText("");
-        } else {
-            mName.setText(name);
-
-            int nameDirection = View.TEXT_DIRECTION_INHERIT;
-            if (nameIsNumber) {
-                nameDirection = View.TEXT_DIRECTION_LTR;
-            }
-            mName.setTextDirection(nameDirection);
-        }
+        setPrimaryName(name, nameIsNumber);
 
         // Set the label (Mobile, Work, etc)
-        if (!TextUtils.isEmpty(label)) {
-            mNumberLabel.setText(label);
-            mNumberLabel.setVisibility(View.VISIBLE);
-        } else {
-            mNumberLabel.setVisibility(View.GONE);
-        }
+        setPrimaryLabel(label);
 
         setDrawableToImageView(mPhoto, photo);
     }
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index cc2e3bf..c068069 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -33,6 +33,8 @@
 import com.android.incallui.service.PhoneNumberService;
 import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
+import com.android.services.telephony.common.CallIdentification;
+import com.google.common.base.Preconditions;
 
 /**
  * Presenter for the Call Card Fragment.
@@ -40,14 +42,13 @@
  * This class listens for changes to InCallState and passes it along to the fragment.
  */
 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
-        implements InCallStateListener, AudioModeListener, ContactInfoCacheCallback {
+        implements InCallStateListener, AudioModeListener {
 
     private static final String TAG = CallCardPresenter.class.getSimpleName();
     private static final long CALL_TIME_UPDATE_INTERVAL = 1000; // in milliseconds
 
     private PhoneNumberService mPhoneNumberService;
     private AudioModeProvider mAudioModeProvider;
-    private ContactInfoCache mContactInfoCache;
     private Call mPrimary;
     private Call mSecondary;
     private ContactCacheEntry mPrimaryContactInfo;
@@ -65,15 +66,33 @@
         });
     }
 
-    public void init(Context context, PhoneNumberService phoneNumberService) {
+
+    public void init(Context context, PhoneNumberService phoneNumberService, Call call) {
+        mContext = Preconditions.checkNotNull(context);
+        mPhoneNumberService = Preconditions.checkNotNull(phoneNumberService);
+        Preconditions.checkNotNull(call);
         mContext = context;
         mPhoneNumberService = phoneNumberService;
+        final CallIdentification identification = call.getIdentification();
+
+        // TODO(klp): Logic to determine which ui field get what data resides in contactInfoCache.
+        // It needs to be moved so it can be re-used.
+        mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, identification,
+                call.getState() == Call.State.INCOMING);
+
+        // start processing lookups right away.
+        startContactInfoSearch(identification, true, false, call.getState() == Call.State.INCOMING);
     }
 
     @Override
     public void onUiReady(CallCardUi ui) {
         super.onUiReady(ui);
 
+        // Contact search may have completed before ui is ready.
+        if (mPrimaryContactInfo != null) {
+            updatePrimaryDisplayInfo(mPrimaryContactInfo, false);
+        }
+
         if (mAudioModeProvider != null) {
             mAudioModeProvider.addListener(this);
         }
@@ -93,7 +112,7 @@
 
     @Override
     public void onStateChange(InCallState state, CallList callList) {
-        Log.d(TAG, "onStateChange()");
+        Log.d(TAG, "onStateChange() " + state);
         final CallCardUi ui = getUi();
         if (ui == null) {
             return;
@@ -118,15 +137,35 @@
         Log.d(this, "Primary call: " + primary);
         Log.d(this, "Secondary call: " + secondary);
 
+        if (primary != null) {
+            if (mPrimary == null || mPrimary.getCallId() != primary.getCallId()) {
+                // primary call has changed
+                mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext,
+                        primary.getIdentification(), primary.getState() == Call.State.INCOMING);
+                updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(primary));
+                startContactInfoSearch(primary.getIdentification(), true,
+                        primary.isConferenceCall(), primary.getState() == Call.State.INCOMING);
+            }
+        }
+
+        if (secondary == null) {
+            // Secondary call may have ended.  Update the ui.
+            mSecondaryContactInfo = null;
+            updateSecondaryDisplayInfo();
+        } else {
+            if (mSecondary == null || mSecondary.getCallId() != secondary.getCallId()) {
+                // secondary call has changed
+                mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext,
+                        secondary.getIdentification(), secondary.getState() == Call.State.INCOMING);
+                updateSecondaryDisplayInfo();
+                startContactInfoSearch(secondary.getIdentification(), false,
+                        secondary.isConferenceCall(), secondary.getState() == Call.State.INCOMING);
+            }
+        }
+
         mPrimary = primary;
         mSecondary = secondary;
 
-        // 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();
-
         // Start/Stop the call time update timer
         if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) {
             Log.d(this, "Starting the calltime timer");
@@ -175,27 +214,62 @@
         }
     }
 
-
-    public void setContactInfoCache(ContactInfoCache cache) {
-        mContactInfoCache = cache;
-        startContactInfoSearch();
-    }
-
     /**
      * 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();
-        }
+    private void startContactInfoSearch(final CallIdentification identification,
+            final boolean isPrimary, final boolean isConference, boolean isIncoming) {
 
-        if (mSecondary != null && mContactInfoCache != null) {
-            mContactInfoCache.findInfo(mSecondary, this);
+        final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
+
+        // See if local contact lookup was already made by the status bar (for incoming calls)
+        final ContactCacheEntry entry = cache.getInfo(identification.getCallId());
+
+        // TODO: un-stable... must use number field to check if a contact was found
+        // because the contactinfocache pre-massages the data into the ui fields.
+        // Need to do massaging outside of contactinfocache.
+        if (entry == null || entry.number == null) {
+            // TODO(klp): currently we can't distinguish between...
+            //   1) a lookup occurred but failed to find a local contact.
+            //   2) a lookup has not occurred.
+            // We need to track it so we can avoid an un-necessary lookup here.
+            Log.d(TAG, "Local contact cache does not contain the contact.  Searching provider.");
+            cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
+                @Override
+                public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+                    // TODO: un-stable... must use label field to check if a contact was found
+                    // because the contactinfocache pre-massages the data into the ui fields.
+                    // Need to do massaging outside of contactinfocache.
+                    if (entry.label == null) {
+                        // Name not found.  Try lookup.
+                        Log.d(TAG, "Local contact not found, performing reverse lookup.");
+                        lookupPhoneNumber(identification.getNumber());
+                    } else {
+                        Log.d(TAG, "Found contact in provider: " + entry);
+                        updateContactEntry(entry, isPrimary, isConference);
+                    }
+                }
+            });
         } else {
-            mSecondaryContactInfo = null;
+            Log.d(TAG, "Found contact in cache: " + entry);
+            updateContactEntry(entry, isPrimary, isConference);
+        }
+    }
+
+    private boolean isConference(Call call) {
+        if (call == null) {
+            return false;
+        }
+        return call.isConferenceCall();
+    }
+
+    private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary,
+            boolean isConference) {
+        if (isPrimary) {
+            mPrimaryContactInfo = entry;
+            updatePrimaryDisplayInfo(entry, isConference);
+        } else {
+            mSecondaryContactInfo = entry;
             updateSecondaryDisplayInfo();
         }
     }
@@ -238,38 +312,25 @@
         return retval;
     }
 
-    /**
-     * Callback received when Contact info data query completes.
-     */
-    @Override
-    public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
-        if (mPrimary != null && mPrimary.getCallId() == callId) {
-            mPrimaryContactInfo = entry;
-            updatePrimaryDisplayInfo();
-            lookupPhoneNumber(mPrimary.getNumber());
-        }
-        if (mSecondary != null && mSecondary.getCallId() == callId) {
-            mSecondaryContactInfo = entry;
-            updateSecondaryDisplayInfo();
-            // TODO(klp): investigate reverse lookup for secondary call.
-        }
-
-    }
-
-    private void updatePrimaryDisplayInfo() {
+    private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) {
+        Log.d(TAG, "Update primary display " + entry);
         final CallCardUi ui = getUi();
         if (ui == null) {
+            // TODO: May also occur if search result comes back after ui is destroyed. Look into
+            // removing that case completely.
+            Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
             return;
         }
 
-        if (mPrimaryContactInfo != null) {
-            final String name = getNameForCall(mPrimaryContactInfo);
-            final String number = getNumberForCall(mPrimaryContactInfo);
-            final boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+        if (entry != null) {
+            final String name = getNameForCall(entry);
+            final String number = getNumberForCall(entry);
+            final boolean nameIsNumber = name != null && name.equals(entry.number);
             final String gatewayLabel = getGatewayLabel();
             final String gatewayNumber = getGatewayNumber();
-            ui.setPrimary(number, name, nameIsNumber, mPrimaryContactInfo.label,
-                    mPrimaryContactInfo.photo, mPrimary.isConferenceCall(), gatewayLabel,
+
+            ui.setPrimary(number, name, nameIsNumber, entry.label,
+                    entry.photo, isConference, gatewayLabel,
                     gatewayNumber);
         } else {
             // reset to nothing (like at end of call)
@@ -278,6 +339,23 @@
 
     }
 
+    private void updateSecondaryDisplayInfo() {
+
+        final CallCardUi ui = getUi();
+        if (ui == null) {
+            return;
+        }
+
+        if (mSecondaryContactInfo != null) {
+            Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
+            ui.setSecondary(true, getNameForCall(mSecondaryContactInfo),
+                    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);
+        }
+    }
+
     public void lookupPhoneNumber(String phoneNumber) {
         if (mPhoneNumberService != null) {
             mPhoneNumberService.getPhoneNumberInfo(phoneNumber,
@@ -291,7 +369,7 @@
                             // TODO(klp): Ui is sometimes null due to something being shutdown.
                             if (getUi() != null) {
                                 if (info.getName() != null) {
-                                    getUi().setName(info.getName());
+                                    getUi().setPrimaryName(info.getName(), false);
                                 }
 
                                 if (info.getImageUrl() != null) {
@@ -333,6 +411,12 @@
     private boolean hasOutgoingGatewayCall() {
         // We only display the gateway information while DIALING so return false for any othe
         // call state.
+        // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
+        // is also called after a contact search completes (call is not present yet).  Split the
+        // UI update so it can receive independent updates.
+        if (mPrimary == null) {
+            return false;
+        }
         return (mPrimary.getState() == Call.State.DIALING &&
                 !TextUtils.isEmpty(mPrimary.getGatewayNumber()) &&
                 !TextUtils.isEmpty(mPrimary.getGatewayPackage()));
@@ -352,7 +436,7 @@
                 protected void onPostExecute(Bitmap bitmap) {
                     // TODO(klp): same as above, figure out why it's null.
                     if (getUi() != null) {
-                        getUi().setImage(bitmap);
+                        getUi().setPrimaryImage(bitmap);
                     }
                 }
 
@@ -382,22 +466,6 @@
         return contactInfo.number;
     }
 
-    private void updateSecondaryDisplayInfo() {
-        final CallCardUi ui = getUi();
-        if (ui == null) {
-            return;
-        }
-
-        if (mSecondaryContactInfo != null) {
-            final String name = getNameForCall(mSecondaryContactInfo);
-            ui.setSecondary(true, getNameForCall(mSecondaryContactInfo),
-                    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);
-        }
-    }
-
     public void setAudioModeProvider(AudioModeProvider audioModeProvider) {
         mAudioModeProvider = audioModeProvider;
         mAudioModeProvider.addListener(this);
@@ -409,8 +477,12 @@
                 Drawable photo, boolean isConference, String gatewayLabel, String gatewayNumber);
         void setSecondary(boolean show, String name, String label, Drawable photo);
         void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn);
+
         void setPrimaryCallElapsedTime(boolean show, String duration);
-        void setName(String name);
-        void setImage(Bitmap bitmap);
+        void setPrimaryName(String name, boolean nameIsNumber);
+        void setPrimaryImage(Bitmap bitmap);
+        void setPrimaryPhoneNumber(String phoneNumber);
+        void setPrimaryLabel(String label);
+        void setPrimaryGateway(String label, String number);
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java
index 1509350..76237e1 100644
--- a/InCallUI/src/com/android/incallui/CallHandlerService.java
+++ b/InCallUI/src/com/android/incallui/CallHandlerService.java
@@ -36,6 +36,8 @@
  */
 public class CallHandlerService extends Service {
 
+    private final static String TAG = CallHandlerService.class.getSimpleName();
+
     private static final int ON_UPDATE_CALL = 1;
     private static final int ON_UPDATE_MULTI_CALL = 2;
     private static final int ON_UPDATE_CALL_WITH_TEXT_RESPONSES = 3;
@@ -56,8 +58,8 @@
         Log.d(this, "onCreate started");
         super.onCreate();
 
-        mCallList = new CallList();
         mMainHandler = new MainHandler();
+        mCallList = CallList.getInstance();
         mAudioModeProvider = new AudioModeProvider();
         mInCallPresenter = InCallPresenter.getInstance();
         mInCallPresenter.setUp(getApplicationContext(), mCallList, mAudioModeProvider);
@@ -105,54 +107,73 @@
 
         @Override
         public void setCallCommandService(ICallCommandService service) {
-            Log.d(CallHandlerService.this, "onConnected: " + service.toString());
-            CallCommandClient.getInstance().setService(service);
+            try {
+                Log.d(CallHandlerService.this, "onConnected: " + service.toString());
+                CallCommandClient.getInstance().setService(service);
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing setCallCommandservice() call", e);
+            }
         }
 
         @Override
         public void onDisconnect(Call call) {
-            Log.d(CallHandlerService.this, "onDisconnected: " + call);
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_DISCONNECT_CALL, 0, 0, call));
+            try {
+                Log.d(CallHandlerService.this, "onDisconnected: " + call);
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_DISCONNECT_CALL, call));
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing onDisconnect() call.", e);
+            }
         }
 
         @Override
         public void onIncoming(Call call, List<String> textResponses) {
-            Log.d(CallHandlerService.this, "onIncomingCall: " + call);
+            try {
+                Log.d(CallHandlerService.this, "onIncomingCall: " + call);
 
-            // TODO(klp): Add text responses to the call object.
-            Map.Entry<Call, List<String> > incomingCall = new AbstractMap.SimpleEntry<Call,
-                    List<String> >(call, textResponses);
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_CALL_WITH_TEXT_RESPONSES,
-                    0, 0, incomingCall));
+                // TODO(klp): Add text responses to the call object.
+                Map.Entry<Call, List<String>> incomingCall
+                        = new AbstractMap.SimpleEntry<Call, List<String>>(call, textResponses);
+                Log.d("TEST", mMainHandler.toString());
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                        ON_UPDATE_CALL_WITH_TEXT_RESPONSES, incomingCall));
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing onIncoming() call.", e);
+            }
         }
 
         @Override
-        public void onUpdate(List<Call> calls, boolean fullUpdate) {
-            Log.d(CallHandlerService.this, "onUpdate " + calls.toString());
+        public void onUpdate(List<Call> calls) {
+            try {
+                Log.d(CallHandlerService.this, "onUpdate " + calls.toString());
 
-            if (Log.VERBOSE) {
-                for (Call c : calls) {
-                    Log.v(this, "Call: " + c);
-                }
+                // TODO(klp): Add use of fullUpdate to message
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_MULTI_CALL, calls));
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing onUpdate() call.", e);
             }
-
-            // TODO(klp): Add use of fullUpdate to message
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_MULTI_CALL, 0, 0, calls));
         }
 
         @Override
         public void onAudioModeChange(int mode) {
-            Log.d(CallHandlerService.this, "onAudioModeChange : " + AudioMode.toString(mode));
-            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_AUDIO_MODE, mode, 0, null));
+            try {
+                Log.d(CallHandlerService.this, "onAudioModeChange : " + AudioMode.toString(mode));
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_AUDIO_MODE, mode, 0, null));
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing onAudioModeChange() call.", e);
+            }
         }
 
         @Override
         public void onSupportedAudioModeChange(int modeMask) {
-            Log.d(CallHandlerService.this, "onSupportedAudioModeChange : " +
-                    AudioMode.toString(modeMask));
+            try {
+                Log.d(CallHandlerService.this, "onSupportedAudioModeChange : " + AudioMode.toString(
+                        modeMask));
 
-            mMainHandler.sendMessage(
-                    mMainHandler.obtainMessage(ON_SUPPORTED_AUDIO_MODE, modeMask, 0, null));
+                mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_SUPPORTED_AUDIO_MODE,
+                        modeMask, 0, null));
+            } catch (Exception e) {
+                Log.e(TAG, "Error processing onSupportedAudioModeChange() call.", e);
+            }
         }
     };
 
@@ -188,7 +209,9 @@
                 mCallList.onUpdate((List<Call>) msg.obj);
                 break;
             case ON_UPDATE_CALL_WITH_TEXT_RESPONSES:
-                mCallList.onUpdate((AbstractMap.SimpleEntry<Call, List<String> >) msg.obj);
+                AbstractMap.SimpleEntry<Call, List<String>> entry
+                        = (AbstractMap.SimpleEntry<Call, List<String>>) msg.obj;
+                mCallList.onIncoming(entry.getKey(), entry.getValue());
                 break;
             case ON_DISCONNECT_CALL:
                 mCallList.onDisconnect((Call) msg.obj);
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index 7890b997..4ce04d3 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -16,6 +16,7 @@
 
 package com.android.incallui;
 
+import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
@@ -25,7 +26,6 @@
 
 import com.android.services.telephony.common.Call;
 
-import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -42,27 +42,27 @@
 
     private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
 
-    private static CallList sInstance;
+    private static CallList sInstance = new CallList();
 
     private final HashMap<Integer, Call> mCallMap = Maps.newHashMap();
     private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap =
             Maps.newHashMap();
     private final Set<Listener> mListeners = Sets.newArraySet();
+    private final HashMap<Integer, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
+            .newHashMap();
+
 
     /**
      * Static singleton accessor method.
      */
-    /*public static synchronized CallList getInstance() {
-        if (sInstance == null) {
-            sInstance = new CallList();
-        }
+    public static CallList getInstance() {
         return sInstance;
-    }*/
+    }
 
     /**
      * Private constructor.  Instance should only be acquired through getInstance().
      */
-    public CallList() {
+    private CallList() {
     }
 
     /**
@@ -89,13 +89,15 @@
     /**
      * Called when a single call has changed.
      */
-    public void onUpdate(AbstractMap.SimpleEntry<Call, List<String> > incomingCall) {
-        Log.d(this, "onUpdate - " + incomingCall.getKey());
+    public void onIncoming(Call call, List<String> textMessages) {
+        Log.d(this, "onIncoming - " + call);
 
-        updateCallInMap(incomingCall.getKey());
-        updateCallTextMap(incomingCall.getKey(), incomingCall.getValue());
+        updateCallInMap(call);
+        updateCallTextMap(call, textMessages);
 
-        notifyListenersOfChange();
+        for (Listener listener : mListeners) {
+            listener.onIncomingCall(call);
+        }
     }
 
     /**
@@ -110,11 +112,50 @@
 
             updateCallInMap(call);
             updateCallTextMap(call, null);
+
+            notifyCallUpdateListeners(call);
         }
 
         notifyListenersOfChange();
     }
 
+    public void notifyCallUpdateListeners(Call call) {
+        final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getCallId());
+        if (listeners != null) {
+            for (CallUpdateListener listener : listeners) {
+                listener.onCallStateChanged(call);
+            }
+        }
+    }
+
+    /**
+     * Add a call update listener for a call id.
+     *
+     * @param callId The call id to get updates for.
+     * @param listener The listener to add.
+     */
+    public void addCallUpdateListener(int callId, CallUpdateListener listener) {
+        List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
+        if (listeners == null) {
+            listeners = Lists.newArrayList();
+            mCallUpdateListenerMap.put(callId, listeners);
+        }
+        listeners.add(listener);
+    }
+
+    /**
+     * Remove a call update listener for a call id.
+     *
+     * @param callId The call id to remove the listener for.
+     * @param listener The listener to remove.
+     */
+    public void removeCallUpdateListener(int callId, CallUpdateListener listener) {
+        List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
+        if (listeners != null) {
+            listeners.remove(listener);
+        }
+    }
+
     public void addListener(Listener listener) {
         Preconditions.checkNotNull(listener);
 
@@ -179,6 +220,19 @@
         return call;
     }
 
+
+    public Call getFirstCall() {
+        // TODO: should we switch to a simple list and pull the first one?
+        Call result = getIncomingCall();
+        if (result == null) {
+            result = getFirstCallWithState(Call.State.DIALING);
+        }
+        if (result == null) {
+            result = getFirstCallWithState(Call.State.ACTIVE);
+        }
+        return result;
+    }
+
     public boolean existsLiveCall() {
         for (Call call : mCallMap.values()) {
             if (!isCallDead(call)) {
@@ -188,8 +242,8 @@
         return false;
     }
 
-    public ArrayList<String> getTextResponses(Call call) {
-        return mCallTextReponsesMap.get(call.getCallId());
+    public ArrayList<String> getTextResponses(int callId) {
+        return mCallTextReponsesMap.get(callId);
     }
 
     /**
@@ -327,5 +381,11 @@
      */
     public interface Listener {
         public void onCallListChange(CallList callList);
+        public void onIncomingCall(Call call);
+    }
+
+    public interface CallUpdateListener {
+        // TODO: refactor and limit arg to be call state.  Caller info is not needed.
+        public void onCallStateChanged(Call call);
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java
index 690b9b7..8d1fc9f 100644
--- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java
+++ b/InCallUI/src/com/android/incallui/CallerInfoUtils.java
@@ -4,6 +4,7 @@
 import android.text.TextUtils;
 
 import com.android.services.telephony.common.Call;
+import com.android.services.telephony.common.CallIdentification;
 
 import java.util.Arrays;
 
@@ -28,40 +29,36 @@
      * 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,
+    public static CallerInfo getCallerInfoForCall(Context context, CallIdentification call,
             CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
-        CallerInfo info = new CallerInfo();
+        CallerInfo info = buildCallerInfo(context, call);
         String number = call.getNumber();
 
-        // Store CNAP information retrieved from the Connection (we want to do this
-        // here regardless of whether the number is empty or not).
-        info.cnapName = call.getCnapName();
-        info.name = info.cnapName;
-        info.numberPresentation = call.getNumberPresentation();
-        info.namePresentation = call.getCnapNamePresentation();
-
         // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
 
+        if (info.numberPresentation == Call.PRESENTATION_ALLOWED) {
+            // Start the query with the number provided from the call.
+            Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
+            CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, number, listener, call);
+        }
+        return info;
+    }
+
+    public static CallerInfo buildCallerInfo(Context context, CallIdentification identification) {
+        CallerInfo info = new CallerInfo();
+
+        // Store CNAP information retrieved from the Connection (we want to do this
+        // here regardless of whether the number is empty or not).
+        info.cnapName = identification.getCnapName();
+        info.name = info.cnapName;
+        info.numberPresentation = identification.getNumberPresentation();
+        info.namePresentation = identification.getCnapNamePresentation();
+
+        String number = identification.getNumber();
         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.
-                Log.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
index e62900e..a80d91c 100644
--- a/InCallUI/src/com/android/incallui/ContactInfoCache.java
+++ b/InCallUI/src/com/android/incallui/ContactInfoCache.java
@@ -16,10 +16,6 @@
 
 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;
@@ -31,7 +27,14 @@
 import android.text.TextUtils;
 
 import com.android.services.telephony.common.Call;
+import com.android.services.telephony.common.CallIdentification;
+import com.android.services.telephony.common.MoreStrings;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -42,73 +45,94 @@
  * 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 {
+public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener {
 
+    private static final String TAG = ContactInfoCache.class.getSimpleName();
     private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
 
     private final Context mContext;
-    private final Map<Integer, SearchEntry> mInfoMap = Maps.newHashMap();
+    private final HashMap<Integer, ContactCacheEntry> mInfoMap = Maps.newHashMap();
+    private final HashMap<Integer, List<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap();
 
-    public ContactInfoCache(Context context) {
+    private static ContactInfoCache sCache = null;
+
+    public static synchronized ContactInfoCache getInstance(Context mContext) {
+        if (sCache == null) {
+            sCache = new ContactInfoCache(mContext);
+        }
+        return sCache;
+    }
+
+    private ContactInfoCache(Context context) {
         mContext = context;
     }
 
+    public ContactCacheEntry getInfo(int callId) {
+        return mInfoMap.get(callId);
+    }
+
+    public static ContactCacheEntry buildCacheEntryFromCall(Context context,
+            CallIdentification identification, boolean isIncoming) {
+        final ContactCacheEntry entry = new ContactCacheEntry();
+
+        // TODO: get rid of caller info.
+        final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, identification);
+        ContactInfoCache.populateCacheEntry(context, info, entry,
+                identification.getNumberPresentation(), isIncoming);
+        return entry;
+    }
+
     /**
      * 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 identification The call identification
      * @param callback The function to call back when the call is found. Can be null.
      */
-    public void findInfo(Call call, ContactInfoCacheCallback callback) {
+    public void findInfo(final CallIdentification identification, final boolean isIncoming,
+            ContactInfoCacheCallback callback) {
         Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
         Preconditions.checkNotNull(callback);
-        Preconditions.checkNotNull(call);
 
-        final SearchEntry entry;
-
+        final int callId = identification.getCallId();
         // If the entry already exists, add callback
-        if (mInfoMap.containsKey(call.getCallId())) {
-            entry = mInfoMap.get(call.getCallId());
+        List<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+        if (callBacks == null) {
 
-            // If this entry is still pending, the callback will also get called when it returns.
-            if (!entry.finished) {
-                entry.addCallback(callback);
-            }
+            // New lookup
+            callBacks = Lists.newArrayList();
+            callBacks.add(callback);
+            mCallBacks.put(callId, callBacks);
+
+            /**
+             * 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.
+             */
+            CallerInfoUtils.getCallerInfoForCall(mContext, identification,
+                    new CallerInfoAsyncQuery.OnQueryCompleteListener() {
+                        @Override
+                        public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+                            int presentationMode = identification.getNumberPresentation();
+                            if (ci.contactExists || ci.isEmergencyNumber() || ci
+                                    .isVoiceMailNumber()) {
+                                presentationMode = Call.PRESENTATION_ALLOWED;
+                            }
+
+                            // This starts the photo load.
+                            final ContactCacheEntry cacheEntry = buildEntry(mContext,
+                                    identification.getCallId(), ci, presentationMode, isIncoming,
+                                    ContactInfoCache.this);
+
+                            // Add the contact info to the cache.
+                            mInfoMap.put(callId, cacheEntry);
+                            sendNotification(identification.getCallId(), cacheEntry);
+                        }
+                    });
         } 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);
+            callBacks.add(callback);
         }
     }
 
@@ -124,30 +148,30 @@
         // TODO (klp): What is this, and why does it need the write_contacts permission?
         // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri);
 
-        final Call call = (Call) cookie;
+        final int callId = (Integer) cookie;
 
-        if (!mInfoMap.containsKey(call.getCallId())) {
+        if (!mInfoMap.containsKey(callId)) {
             Log.e(this, "Image Load received for empty search entry.");
             return;
         }
 
-        final SearchEntry entry = mInfoMap.get(call.getCallId());
+        final ContactCacheEntry entry = mInfoMap.get(callId);
 
         Log.d(this, "setting photo for entry: ", entry);
 
         // TODO (klp): Handle conference calls
         if (photo != null) {
             Log.v(this, "direct drawable: ", photo);
-            entry.info.photo = photo;
+            entry.photo = photo;
         } else if (photoIcon != null) {
             Log.v(this, "photo icon: ", photoIcon);
-            entry.info.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+            entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
         } else {
             Log.v(this, "unknown photo");
-            entry.info.photo = null;
+            entry.photo = null;
         }
 
-        sendNotification(entry);
+        sendNotification(callId, entry);
     }
 
     /**
@@ -155,33 +179,63 @@
      */
     public void clearCache() {
         mInfoMap.clear();
+        mCallBacks.clear();
+    }
+
+    private static ContactCacheEntry buildEntry(Context context, int callId,
+            CallerInfo info, int presentation, boolean isIncoming,
+            ContactsAsyncHelper.OnImageLoadCompleteListener imageLoadListener) {
+        // The actual strings we're going to display onscreen:
+        Drawable photo = null;
+
+        final ContactCacheEntry cce = new ContactCacheEntry();
+        populateCacheEntry(context, info, cce, presentation, isIncoming);
+
+        // This will only be true for emergency numbers
+        if (info.photoResource != 0) {
+            photo = context.getResources().getDrawable(info.photoResource);
+        } else if (info.isCachedPhotoCurrent) {
+            if (info.cachedPhoto != null) {
+                photo = info.cachedPhoto;
+            } else {
+                photo = context.getResources().getDrawable(R.drawable.picture_unknown);
+            }
+        } else {
+            Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
+            Log.d(TAG, "- got personUri: '" + personUri + "', based on info.person_id: " +
+                    info.person_id);
+
+            if (personUri == null) {
+                Log.v(TAG, "personUri is null. Just use unknown picture.");
+                photo = context.getResources().getDrawable(R.drawable.picture_unknown);
+            } else {
+                Log.d(TAG, "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,
+                        context, personUri, imageLoadListener, callId);
+
+                // 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);
+            }
+        }
+
+        cce.photo = photo;
+        return cce;
     }
 
     /**
-     * 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.
+     * Populate a cache entry from a caller identification (which got converted into a caller info).
      */
-    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:
+    public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce,
+            int presentation, boolean isIncoming) {
+        Preconditions.checkNotNull(info);
         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
@@ -219,21 +273,20 @@
                 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);
-                    Log.d(this, "  ==> no name *or* number! displayName = " + displayName);
+                    displayName = getPresentationString(context, presentation);
+                    Log.d(TAG, "  ==> 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);
-                    Log.d(this, "  ==> presentation not allowed! displayName = " + displayName);
+                    displayName = getPresentationString(context, presentation);
+                    Log.d(TAG, "  ==> 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;
-                    Log.d(this, "  ==> cnapName available: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
+                    Log.d(TAG, "  ==> 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,
@@ -245,16 +298,16 @@
 
                     // ...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)) {
+                    if (isIncoming) {
                         // 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
-                        Log.d(this, "Geodescrption: " + info.geoDescription);
+                        Log.d(TAG, "Geodescrption: " + info.geoDescription);
                     }
 
-                    Log.d(this, "  ==>  no name; falling back to number: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
+                    Log.d(TAG,
+                            "  ==>  no name; falling back to number: displayName '" + displayName + "', displayNumber '" + displayNumber + "'");
                 }
             } else {
                 // We do have a valid "name" in the CallerInfo. Display that
@@ -263,79 +316,44 @@
                     // 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);
-                    Log.d(this, "  ==> valid name, but presentation not allowed!"
-                            + " displayName = " + displayName);
+                    displayName = getPresentationString(context, presentation);
+                    Log.d(TAG,
+                            "  ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
                 } else {
                     displayName = info.name;
                     displayNumber = number;
                     label = info.phoneLabel;
-                    Log.d(this, "  ==>  name is present in CallerInfo: displayName '"
-                            + displayName + "', displayNumber '" + displayNumber + "'");
+                    Log.d(TAG, "  ==>  name is present in CallerInfo: displayName '" + displayName
+                            + "', displayNumber '" + displayNumber + "'");
                 }
             }
-            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
-            Log.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) {
-                Log.v(this, "personUri is null. Just use unknown picture.");
-                photo = mContext.getResources().getDrawable(R.drawable.picture_unknown);
-            } else {
-                Log.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);
+    private void sendNotification(int callId, ContactCacheEntry entry) {
+        List<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+        if (callBacks != null) {
+            for (ContactInfoCacheCallback callBack : callBacks) {
+                callBack.onContactInfoComplete(callId, entry);
+            }
         }
     }
 
     /**
      * Gets name strings based on some special presentation modes.
      */
-    private String getPresentationString(int presentation) {
-        String name = mContext.getString(R.string.unknown);
+    private static String getPresentationString(Context context, int presentation) {
+        String name = context.getString(R.string.unknown);
         if (presentation == Call.PRESENTATION_RESTRICTED) {
-            name = mContext.getString(R.string.private_num);
+            name = context.getString(R.string.private_num);
         } else if (presentation == Call.PRESENTATION_PAYPHONE) {
-            name = mContext.getString(R.string.payphone);
+            name = context.getString(R.string.payphone);
         }
         return name;
     }
@@ -352,31 +370,15 @@
         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;
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("name", MoreStrings.toSafeString(name))
+                    .add("number", MoreStrings.toSafeString(number))
+                    .add("label", label)
+                    .add("photo", photo)
+                    .toString();
         }
     }
 }
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index ec5002f..5915ecf 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -292,14 +292,11 @@
                 mainPresenter.getAudioModeProvider());
         mCallButtonFragment.getPresenter().setProximitySensor(
                 mainPresenter.getProximitySensor());
-        mCallCardFragment.getPresenter().setAudioModeProvider(
-                mainPresenter.getAudioModeProvider());
-        mCallCardFragment.getPresenter().setContactInfoCache(
-                mainPresenter.getContactInfoCache());
+        final CallCardPresenter presenter = mCallCardFragment.getPresenter();
+        presenter.setAudioModeProvider(mainPresenter.getAudioModeProvider());
 
         mainPresenter.addListener(mCallButtonFragment.getPresenter());
         mainPresenter.addListener(mCallCardFragment.getPresenter());
-        mainPresenter.addListener(mAnswerFragment.getPresenter());
 
         // setting activity should be last thing in setup process
         mainPresenter.setActivity(this);
@@ -311,7 +308,6 @@
 
         mainPresenter.removeListener(mCallButtonFragment.getPresenter());
         mainPresenter.removeListener(mCallCardFragment.getPresenter());
-        mainPresenter.removeListener(mAnswerFragment.getPresenter());
 
         mainPresenter.setActivity(null);
     }
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 8bb0973..3aa717c 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -23,7 +23,9 @@
 import android.content.Intent;
 
 import com.android.services.telephony.common.Call;
+import com.google.common.collect.Lists;
 
+import java.util.ArrayList;
 import java.util.Set;
 
 /**
@@ -40,6 +42,7 @@
     private static InCallPresenter sInCallPresenter;
 
     private final Set<InCallStateListener> mListeners = Sets.newHashSet();
+    private final ArrayList<IncomingCallListener> mIncomingCallListeners = Lists.newArrayList();
 
     private AudioModeProvider mAudioModeProvider;
     private StatusBarNotifier mStatusBarNotifier;
@@ -65,10 +68,11 @@
         mCallList = callList;
         mCallList.addListener(this);
 
-        mContactInfoCache = new ContactInfoCache(context);
+        mContactInfoCache = ContactInfoCache.getInstance(context);
 
         mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache, mCallList);
         addListener(mStatusBarNotifier);
+        addIncomingCallListener(mStatusBarNotifier);
 
         mAudioModeProvider = audioModeProvider;
 
@@ -138,6 +142,20 @@
     }
 
     /**
+     * Called when there is a new incoming call.
+     *
+     * @param call
+     */
+    @Override
+    public void onIncomingCall(Call call) {
+        mInCallState = startOrFinishUi(InCallState.INCOMING);
+
+        for (IncomingCallListener listener : mIncomingCallListeners) {
+            listener.onIncomingCall(call);
+        }
+    }
+
+    /**
      * Given the call list, return the state in which the in-call screen should be.
      */
     public static InCallState getPotentialStateFromCallList(CallList callList) {
@@ -156,6 +174,11 @@
         return newState;
     }
 
+    public void addIncomingCallListener(IncomingCallListener listener) {
+        Preconditions.checkNotNull(listener);
+        mIncomingCallListeners.add(listener);
+    }
+
     public void addListener(InCallStateListener listener) {
         Preconditions.checkNotNull(listener);
         mListeners.add(listener);
@@ -375,4 +398,8 @@
         // TODO(klp): Enhance state to contain the call objects instead of passing CallList
         public void onStateChange(InCallState state, CallList callList);
     }
+
+    public interface IncomingCallListener {
+        public void onIncomingCall(Call call);
+    }
 }
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
index 466c7b2..f8289c9 100644
--- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java
+++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
@@ -16,6 +16,7 @@
 
 package com.android.incallui;
 
+import com.android.services.telephony.common.CallIdentification;
 import com.google.common.base.Preconditions;
 
 import android.app.Notification;
@@ -37,7 +38,7 @@
  * This class adds Notifications to the status bar for the in-call experience.
  */
 public class StatusBarNotifier implements InCallPresenter.InCallStateListener,
-        ContactInfoCacheCallback {
+        InCallPresenter.IncomingCallListener {
     // notification types
     private static final int IN_CALL_NOTIFICATION = 1;
 
@@ -71,12 +72,25 @@
         updateNotification(state, callList);
     }
 
-    /**
-     * Called after the Contact Info query has finished.
-     */
     @Override
-    public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
-        updateNotification(mInCallState, mCallList);
+    public void onIncomingCall(final Call call) {
+        final ContactCacheEntry entry = ContactInfoCache.buildCacheEntryFromCall(mContext,
+                call.getIdentification(), true);
+
+        // Initial update with no contact information.
+        buildAndSendNotification(InCallState.INCOMING, call, entry, false);
+
+        // 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.getIdentification(), true, new ContactInfoCacheCallback() {
+            @Override
+            public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
+                buildAndSendNotification(InCallState.INCOMING, call, entry, false);
+            }
+        });
     }
 
     /**
@@ -160,28 +174,24 @@
         Log.d(this, "updateInCallNotification(allowFullScreenIntent = "
                 + allowFullScreenIntent + ")...");
 
-        if (shouldSuppressNotification(state, callList)) {
+        final Call call = getCallToShow(callList);
+        if (shouldSuppressNotification(state, call)) {
             cancelInCall();
             return;
         }
 
-        final Call call = getCallToShow(callList);
         if (call == null) {
             Log.wtf(this, "No call for the notification!");
+            return;
         }
 
-        // 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);
-            }
-        });
-
+        // Contact info should have already been done on incoming calls.
+        ContactCacheEntry entry = mContactInfoCache.getInfo(call.getCallId());
+        if (entry == null) {
+            entry = ContactInfoCache.buildCacheEntryFromCall(mContext, call.getIdentification(),
+                    state == InCallState.INCOMING);
+        }
+        buildAndSendNotification(state, call, entry, allowFullScreenIntent);
     }
 
     /**
@@ -426,7 +436,7 @@
     /**
      * Returns true if notification should not be shown in the current state.
      */
-    private boolean shouldSuppressNotification(InCallState state, CallList callList) {
+    private boolean shouldSuppressNotification(InCallState state, Call call) {
         // 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*
@@ -440,7 +450,6 @@
 
         // 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;
         }