Fix the bug that quick contact cannot be removed. am: 0065720a97
am: 7976e7a03b

* commit '7976e7a03b6380f72452c4750baafb1240ab5b5f':
  Fix the bug that quick contact cannot be removed.

Change-Id: I4d155124b660e68a2c768b5096b2c31090faf622
diff --git a/InCallUI/res/layout/call_button_fragment.xml b/InCallUI/res/layout/call_button_fragment.xml
index 6dbfbf7..802e3de 100644
--- a/InCallUI/res/layout/call_button_fragment.xml
+++ b/InCallUI/res/layout/call_button_fragment.xml
@@ -126,7 +126,7 @@
         <ToggleButton android:id="@+id/pauseVideoButton"
             style="@style/InCallCompoundButton"
             android:background="@drawable/btn_compound_video_off"
-            android:contentDescription="@string/onscreenPauseVideoText"
+            android:contentDescription="@string/onscreenTurnOffCameraText"
             android:visibility="gone" />
 
         <!-- "Change to audio call" for video calls. -->
diff --git a/InCallUI/res/values/strings.xml b/InCallUI/res/values/strings.xml
index 57a1b53..84eb14c 100644
--- a/InCallUI/res/values/strings.xml
+++ b/InCallUI/res/values/strings.xml
@@ -218,6 +218,17 @@
          The user will be able to send text messages using the phone number.
          [CHAR LIMIT=12] -->
     <string name="notification_missedCall_message">Message</string>
+    <!-- The "label" of the in-call Notification for an ongoing external call.
+         External calls are a representation of a call which is in progress on the user's other
+         device (e.g. another phone or a watch).
+         [CHAR LIMIT=60] -->
+    <string name="notification_external_call">Ongoing call on another device</string>
+    <!-- Notification action displayed for external call notifications.  External calls are a
+         representation of a call which is in progress on the user's other device (e.g. another
+         phone or a watch).  The "transfer call" action initiates the process of transferring an
+         external call to the current device.
+         [CHAR LIMIT=30] -->
+    <string name="notification_transfer_call">Transfer Call</string>
 
     <!-- In-call screen: call failure message displayed in an error dialog -->
     <string name="incall_error_power_off">To place a call, first turn off Airplane mode.</string>
@@ -313,8 +324,10 @@
     <!-- Text for the onscreen "Switch camera" button. When clicked, this switches the user's camera
          for video calling between the front-facing camera and the back-facing camera. -->
     <string name="onscreenSwitchCameraText">Switch camera</string>
-    <!-- Text for the onscreen "Pause video" button. -->
-    <string name="onscreenPauseVideoText">Pause video</string>
+    <!-- Text for the onscreen "turn on camera" button. -->
+    <string name="onscreenTurnOnCameraText">Turn on camera</string>
+    <!-- Text for the onscreen "turn off camera" button. -->
+    <string name="onscreenTurnOffCameraText">Turn off camera</string>
     <!-- Text for the onscreen overflow button, to see additional actions which can be done. -->
     <string name="onscreenOverflowText">More options</string>
 
diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java
index 6757268..883b54f 100644
--- a/InCallUI/src/com/android/incallui/AnswerPresenter.java
+++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java
@@ -128,10 +128,7 @@
     @Override
     public void onUpgradeToVideo(Call call) {
         Log.d(this, "onUpgradeToVideo: " + this + " call=" + call);
-        if (getUi() == null) {
-            Log.d(this, "onUpgradeToVideo ui is null");
-            return;
-        }
+        showAnswerUi(true);
         boolean isUpgradePending = isVideoUpgradePending(call);
         InCallPresenter inCallPresenter = InCallPresenter.getInstance();
         if (isUpgradePending
diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java
index 447c34c..d552ecf 100644
--- a/InCallUI/src/com/android/incallui/Call.java
+++ b/InCallUI/src/com/android/incallui/Call.java
@@ -33,6 +33,8 @@
 import android.text.TextUtils;
 
 import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.compat.CallSdkCompat;
+import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.compat.SdkVersionOverride;
 import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
 import com.android.contacts.common.testing.NeededForTesting;
@@ -400,13 +402,30 @@
         setState(state);
     }
 
+    /**
+     * Creates a new instance of a {@link Call}.  Registers a callback for
+     * {@link android.telecom.Call} events.
+     */
     public Call(android.telecom.Call telecomCall) {
+        this(telecomCall, true /* registerCallback */);
+    }
+
+    /**
+     * Creates a new instance of a {@link Call}.  Optionally registers a callback for
+     * {@link android.telecom.Call} events.
+     *
+     * Intended for use when creating a {@link Call} instance for use with the
+     * {@link ContactInfoCache}, where we do not want to register callbacks for the new call.
+     */
+    public Call(android.telecom.Call telecomCall, boolean registerCallback) {
         mTelecomCall = telecomCall;
         mId = ID_PREFIX + Integer.toString(sIdCounter++);
 
-        updateFromTelecomCall();
+        updateFromTelecomCall(registerCallback);
 
-        mTelecomCall.registerCallback(mTelecomCallCallback);
+        if (registerCallback) {
+            mTelecomCall.registerCallback(mTelecomCallCallback);
+        }
 
         mTimeAddedMs = System.currentTimeMillis();
     }
@@ -426,7 +445,8 @@
     private void update() {
         Trace.beginSection("Update");
         int oldState = getState();
-        updateFromTelecomCall();
+        // We want to potentially register a video call callback here.
+        updateFromTelecomCall(true /* registerCallback */);
         if (oldState != getState() && getState() == Call.State.DISCONNECTED) {
             CallList.getInstance().onDisconnect(this);
         } else {
@@ -435,7 +455,7 @@
         Trace.endSection();
     }
 
-    private void updateFromTelecomCall() {
+    private void updateFromTelecomCall(boolean registerCallback) {
         Log.d(this, "updateFromTelecomCall: " + mTelecomCall.toString());
         final int translatedState = translateState(mTelecomCall.getState());
         if (mState != State.BLOCKED) {
@@ -444,7 +464,7 @@
             maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
         }
 
-        if (mTelecomCall.getVideoCall() != null) {
+        if (registerCallback && mTelecomCall.getVideoCall() != null) {
             if (mVideoCallCallback == null) {
                 mVideoCallCallback = new InCallVideoCallCallback(this);
             }
@@ -883,9 +903,46 @@
     }
 
     /**
+     * Determines if the call is an external call.
+     *
+     * An external call is one which does not exist locally for the
+     * {@link android.telecom.ConnectionService} it is associated with.
+     *
+     * External calls are only supported in N and higher.
+     *
+     * @return {@code true} if the call is an external call, {@code false} otherwise.
+     */
+    public boolean isExternalCall() {
+        return CompatUtils.isNCompatible() &&
+                hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
+    }
+
+    /**
+     * Determines if the external call is pullable.
+     *
+     * An external call is one which does not exist locally for the
+     * {@link android.telecom.ConnectionService} it is associated with.  An external call may be
+     * "pullable", which means that the user can request it be transferred to the current device.
+     *
+     * External calls are only supported in N and higher.
+     *
+     * @return {@code true} if the call is an external call, {@code false} otherwise.
+     */
+    public boolean isPullableExternalCall() {
+        return CompatUtils.isNCompatible() &&
+                (mTelecomCall.getDetails().getCallCapabilities()
+                        & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL)
+                        == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL;
+    }
+
+    /**
      * Logging utility methods
      */
     public void logCallInitiationType() {
+        if (isExternalCall()) {
+            return;
+        }
+
         if (getState() == State.INCOMING) {
             getLogState().callInitiationMethod = LogState.INITIATION_INCOMING;
         } else if (getIntentExtras() != null) {
@@ -903,11 +960,12 @@
             return String.valueOf(mId);
         }
 
-        return String.format(Locale.US, "[%s, %s, %s, children:%s, parent:%s, conferenceable:%s, " +
-                "videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+        return String.format(Locale.US, "[%s, %s, %s, %s, children:%s, parent:%s, " +
+                "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
                 mId,
                 State.toString(getState()),
                 Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
+                Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()),
                 mChildCallIds,
                 getParentId(),
                 this.mTelecomCall.getConferenceableCalls(),
diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java
index 5a25b6a..6b633ea 100644
--- a/InCallUI/src/com/android/incallui/CallButtonFragment.java
+++ b/InCallUI/src/com/android/incallui/CallButtonFragment.java
@@ -65,7 +65,6 @@
         implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener,
         View.OnClickListener {
 
-    private static final int INVALID_INDEX = -1;
     private int mButtonMaxVisible;
     // The button is currently visible in the UI
     private static final int BUTTON_VISIBLE = 1;
@@ -182,7 +181,7 @@
         super.onActivityCreated(savedInstanceState);
 
         // set the buttons
-        updateAudioButtons(getPresenter().getSupportedAudio());
+        updateAudioButtons();
     }
 
     @Override
@@ -425,8 +424,14 @@
     }
 
     @Override
-    public void setVideoPaused(boolean isPaused) {
-        mPauseVideoButton.setSelected(isPaused);
+    public void setVideoPaused(boolean isVideoPaused) {
+        mPauseVideoButton.setSelected(isVideoPaused);
+
+        if (isVideoPaused) {
+            mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOnCameraText));
+        } else {
+            mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOffCameraText));
+        }
     }
 
     @Override
@@ -505,7 +510,7 @@
 
     @Override
     public void setAudio(int mode) {
-        updateAudioButtons(getPresenter().getSupportedAudio());
+        updateAudioButtons();
         refreshAudioModePopup();
 
         if (mPrevAudioMode != mode) {
@@ -516,7 +521,7 @@
 
     @Override
     public void setSupportedAudio(int modeMask) {
-        updateAudioButtons(modeMask);
+        updateAudioButtons();
         refreshAudioModePopup();
     }
 
@@ -555,7 +560,7 @@
     public void onDismiss(PopupMenu menu) {
         Log.d(this, "- onDismiss: " + menu);
         mAudioModePopupVisible = false;
-        updateAudioButtons(getPresenter().getSupportedAudio());
+        updateAudioButtons();
     }
 
     /**
@@ -600,7 +605,7 @@
      * Updates the audio button so that the appriopriate visual layers
      * are visible based on the supported audio formats.
      */
-    private void updateAudioButtons(int supportedModes) {
+    private void updateAudioButtons() {
         final boolean bluetoothSupported = isSupported(CallAudioState.ROUTE_BLUETOOTH);
         final boolean speakerSupported = isSupported(CallAudioState.ROUTE_SPEAKER);
 
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index e8c2d4b..df1cd66 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -366,7 +366,6 @@
     private void updateButtonsState(Call call) {
         Log.v(this, "updateButtonsState");
         final CallButtonUi ui = getUi();
-
         final boolean isVideo = VideoUtils.isVideoCall(call);
 
         // Common functionality (audio, hold, etc).
@@ -398,6 +397,9 @@
         ui.showButton(BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
         ui.showButton(BUTTON_SWITCH_CAMERA, isVideo);
         ui.showButton(BUTTON_PAUSE_VIDEO, isVideo);
+        if (isVideo) {
+            getUi().setVideoPaused(!VideoUtils.isTransmissionEnabled(call));
+        }
         ui.showButton(BUTTON_DIALPAD, true);
         ui.showButton(BUTTON_MERGE, showMerge);
 
diff --git a/InCallUI/src/com/android/incallui/ExternalCallList.java b/InCallUI/src/com/android/incallui/ExternalCallList.java
new file mode 100644
index 0000000..06e0bb9
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ExternalCallList.java
@@ -0,0 +1,105 @@
+package com.android.incallui;
+
+import com.google.common.base.Preconditions;
+
+import com.android.contacts.common.compat.CallSdkCompat;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.telecom.Call;
+import android.util.ArraySet;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks the external calls known to the InCall UI.
+ *
+ * External calls are those with {@link android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}.
+ */
+public class ExternalCallList {
+
+    public interface ExternalCallListener {
+        void onExternalCallAdded(Call call);
+        void onExternalCallRemoved(Call call);
+        void onExternalCallUpdated(Call call);
+    }
+
+    /**
+     * Handles {@link android.telecom.Call.Callback} callbacks.
+     */
+    private final Call.Callback mTelecomCallCallback = new Call.Callback() {
+        @Override
+        public void onDetailsChanged(Call call, Call.Details details) {
+            notifyExternalCallUpdated(call);
+        }
+    };
+
+    private final Set<Call> mExternalCalls = new ArraySet<>();
+    private final Set<ExternalCallListener> mExternalCallListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<ExternalCallListener, Boolean>(8, 0.9f, 1));
+
+    /**
+     * Begins tracking an external call and notifies listeners of the new call.
+     */
+    public void onCallAdded(Call telecomCall) {
+        Preconditions.checkArgument(telecomCall.getDetails()
+                .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL));
+        mExternalCalls.add(telecomCall);
+        telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper()));
+        notifyExternalCallAdded(telecomCall);
+    }
+
+    /**
+     * Stops tracking an external call and notifies listeners of the removal of the call.
+     */
+    public void onCallRemoved(Call telecomCall) {
+        Preconditions.checkArgument(mExternalCalls.contains(telecomCall));
+        mExternalCalls.remove(telecomCall);
+        telecomCall.unregisterCallback(mTelecomCallCallback);
+        notifyExternalCallRemoved(telecomCall);
+    }
+
+    /**
+     * Adds a new listener to external call events.
+     */
+    public void addExternalCallListener(ExternalCallListener listener) {
+        mExternalCallListeners.add(Preconditions.checkNotNull(listener));
+    }
+
+    /**
+     * Removes a listener to external call events.
+     */
+    public void removeExternalCallListener(ExternalCallListener listener) {
+        Preconditions.checkArgument(mExternalCallListeners.contains(listener));
+        mExternalCallListeners.remove(Preconditions.checkNotNull(listener));
+    }
+
+    /**
+     * Notifies listeners of the addition of a new external call.
+     */
+    private void notifyExternalCallAdded(Call call) {
+        for (ExternalCallListener listener : mExternalCallListeners) {
+            listener.onExternalCallAdded(call);
+        }
+    }
+
+    /**
+     * Notifies listeners of the removal of an external call.
+     */
+    private void notifyExternalCallRemoved(Call call) {
+        for (ExternalCallListener listener : mExternalCallListeners) {
+            listener.onExternalCallRemoved(call);
+        }
+    }
+
+    /**
+     * Notifies listeners of changes to an external call.
+     */
+    private void notifyExternalCallUpdated(Call call) {
+        for (ExternalCallListener listener : mExternalCallListeners) {
+            listener.onExternalCallUpdated(call);
+        }
+    }
+}
diff --git a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java
new file mode 100644
index 0000000..40a2e02
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2016 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.common.base.Preconditions;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.CallSdkCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.R;
+import com.android.incallui.util.TelecomCallUtil;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import java.util.Map;
+
+/**
+ * Handles the display of notifications for "external calls".
+ *
+ * External calls are a representation of a call which is in progress on the user's other device
+ * (e.g. another phone, or a watch).
+ */
+public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
+
+    /**
+     * Tag used with the notification manager to uniquely identify external call notifications.
+     */
+    private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+
+    /**
+     * Represents a call and associated cached notification data.
+     */
+    private static class NotificationInfo {
+        private final Call mCall;
+        private final int mNotificationId;
+        @Nullable private String mContentTitle;
+        @Nullable private Bitmap mLargeIcon;
+        @Nullable private String mPersonReference;
+
+        public NotificationInfo(Call call, int notificationId) {
+            Preconditions.checkNotNull(call);
+            mCall = call;
+            mNotificationId = notificationId;
+        }
+
+        public Call getCall() {
+            return mCall;
+        }
+
+        public int getNotificationId() {
+            return mNotificationId;
+        }
+
+        public @Nullable String getContentTitle() {
+            return mContentTitle;
+        }
+
+        public @Nullable Bitmap getLargeIcon() {
+            return mLargeIcon;
+        }
+
+        public @Nullable String getPersonReference() {
+            return mPersonReference;
+        }
+
+        public void setContentTitle(@Nullable String contentTitle) {
+            mContentTitle = contentTitle;
+        }
+
+        public void setLargeIcon(@Nullable Bitmap largeIcon) {
+            mLargeIcon = largeIcon;
+        }
+
+        public void setPersonReference(@Nullable String personReference) {
+            mPersonReference = personReference;
+        }
+    }
+
+    private final Context mContext;
+    private final ContactInfoCache mContactInfoCache;
+    private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
+    private int mNextUniqueNotificationId;
+    private ContactsPreferences mContactsPreferences;
+
+    /**
+     * Initializes a new instance of the external call notifier.
+     */
+    public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) {
+        mContext = Preconditions.checkNotNull(context);
+        mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+        mContactInfoCache = Preconditions.checkNotNull(contactInfoCache);
+    }
+
+    /**
+     * Handles the addition of a new external call by showing a new notification.
+     * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}.
+     */
+    @Override
+    public void onExternalCallAdded(android.telecom.Call call) {
+        Log.i(this, "onExternalCallAdded " + call);
+        Preconditions.checkArgument(!mNotifications.containsKey(call));
+        NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++);
+        mNotifications.put(call, info);
+
+        showNotifcation(info);
+    }
+
+    /**
+     * Handles the removal of an external call by hiding its associated notification.
+     * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}.
+     */
+    @Override
+    public void onExternalCallRemoved(android.telecom.Call call) {
+        Log.i(this, "onExternalCallRemoved " + call);
+
+        dismissNotification(call);
+    }
+
+    /**
+     * Handles updates to an external call.
+     */
+    @Override
+    public void onExternalCallUpdated(Call call) {
+        Preconditions.checkArgument(mNotifications.containsKey(call));
+        postNotification(mNotifications.get(call));
+    }
+
+    /**
+     * Initiates a call pull given a notification ID.
+     *
+     * @param notificationId The notification ID associated with the external call which is to be
+     *                       pulled.
+     */
+    public void pullExternalCall(int notificationId) {
+        for (NotificationInfo info : mNotifications.values()) {
+            if (info.getNotificationId() == notificationId) {
+                CallSdkCompat.pullExternalCall(info.getCall());
+                return;
+            }
+        }
+    }
+
+    /**
+     * Shows a notification for a new external call.  Performs a contact cache lookup to find any
+     * associated photo and information for the call.
+     */
+    private void showNotifcation(final NotificationInfo info) {
+        // 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.
+        com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(),
+                false /* registerCallback */);
+
+        mContactInfoCache.findInfo(incallCall, false /* isIncoming */,
+                new ContactInfoCache.ContactInfoCacheCallback() {
+                    @Override
+                    public void onContactInfoComplete(String callId,
+                            ContactInfoCache.ContactCacheEntry entry) {
+
+                        // Ensure notification still exists as the external call could have been
+                        // removed during async contact info lookup.
+                        if (mNotifications.containsKey(info.getCall())) {
+                            saveContactInfo(info, entry);
+                        }
+                    }
+
+                    @Override
+                    public void onImageLoadComplete(String callId,
+                            ContactInfoCache.ContactCacheEntry entry) {
+
+                        // Ensure notification still exists as the external call could have been
+                        // removed during async contact info lookup.
+                        if (mNotifications.containsKey(info.getCall())) {
+                            savePhoto(info, entry);
+                        }
+                    }
+
+                    @Override
+                    public void onContactInteractionsInfoComplete(String callId,
+                            ContactInfoCache.ContactCacheEntry entry) {
+                    }
+                });
+    }
+
+    /**
+     * Dismisses a notification for an external call.
+     */
+    private void dismissNotification(Call call) {
+        Preconditions.checkArgument(mNotifications.containsKey(call));
+
+        NotificationManager notificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+
+        mNotifications.remove(call);
+    }
+
+    /**
+     * Attempts to build a large icon to use for the notification based on the contact info and
+     * post the updated notification to the notification manager.
+     */
+    private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+        Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall());
+        if (largeIcon != null) {
+            largeIcon = getRoundedIcon(mContext, largeIcon);
+        }
+        info.setLargeIcon(largeIcon);
+        postNotification(info);
+    }
+
+    /**
+     * Builds and stores the contact information the notification will display and posts the updated
+     * notification to the notification manager.
+     */
+    private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+        info.setContentTitle(getContentTitle(mContext, mContactsPreferences,
+                entry, info.getCall()));
+        info.setPersonReference(getPersonReference(entry, info.getCall()));
+        postNotification(info);
+    }
+
+    /**
+     * Rebuild an existing or show a new notification given {@link NotificationInfo}.
+     */
+    private void postNotification(NotificationInfo info) {
+        Log.i(this, "postNotification : " + info.getContentTitle());
+        Notification.Builder builder = new Notification.Builder(mContext);
+        // Set notification as ongoing since calls are long-running versus a point-in-time notice.
+        builder.setOngoing(true);
+        // Make the notification prioritized over the other normal notifications.
+        builder.setPriority(Notification.PRIORITY_HIGH);
+        // Set the content ("Ongoing call on another device")
+        builder.setContentText(mContext.getString(R.string.notification_external_call));
+        builder.setSmallIcon(R.drawable.ic_call_white_24dp);
+        builder.setContentTitle(info.getContentTitle());
+        builder.setLargeIcon(info.getLargeIcon());
+        builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+        builder.addPerson(info.getPersonReference());
+
+        // Where the external call supports being transferred to the local device, add an action
+        // to the notification to initiate the call pull process.
+        if ((info.getCall().getDetails().getCallCapabilities()
+                & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL)
+                == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) {
+
+            Intent intent = new Intent(
+                    NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext,
+                    NotificationBroadcastReceiver.class);
+            intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID,
+                    info.getNotificationId());
+
+            builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp,
+                    mContext.getText(R.string.notification_transfer_call),
+                    PendingIntent.getBroadcast(mContext, 0, intent, 0)).build());
+        }
+
+        /**
+         * This builder is used for the notification shown when the device is locked and the user
+         * has set their notification settings to 'hide sensitive content'
+         * {@see Notification.Builder#setPublicVersion}.
+         */
+        Notification.Builder publicBuilder = new Notification.Builder(mContext);
+        publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp);
+        publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+        builder.setPublicVersion(publicBuilder.build());
+        Notification notification = builder.build();
+
+        NotificationManager notificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+    }
+
+    /**
+     * Finds a large icon to display in a notification for a call.  For conference calls, a
+     * conference call icon is used, otherwise if contact info is specified, the user's contact
+     * photo or avatar is used.
+     *
+     * @param context The context.
+     * @param contactInfo The contact cache info.
+     * @param call The call.
+     * @return The large icon to use for the notification.
+     */
+    private @Nullable Bitmap getLargeIconToDisplay(Context context,
+            ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
+
+        Bitmap largeIcon = null;
+        if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) &&
+                !call.getDetails()
+                        .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+            largeIcon = BitmapFactory.decodeResource(context.getResources(),
+                    R.drawable.img_conference);
+        }
+        if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+            largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+        }
+        return largeIcon;
+    }
+
+    /**
+     * Given a bitmap, returns a rounded version of the icon suitable for display in a notification.
+     *
+     * @param context The context.
+     * @param bitmap The bitmap to round.
+     * @return The rounded bitmap.
+     */
+    private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) {
+        if (bitmap == null) {
+            return null;
+        }
+        final int height = (int) context.getResources().getDimension(
+                android.R.dimen.notification_large_icon_height);
+        final int width = (int) context.getResources().getDimension(
+                android.R.dimen.notification_large_icon_width);
+        return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+    }
+
+    /**
+     * Builds a notification content title for a call.  If the call is a conference call, it is
+     * identified as such.  Otherwise an attempt is made to show an associated contact name or
+     * phone number.
+     *
+     * @param context The context.
+     * @param contactsPreferences Contacts preferences, used to determine the preferred formatting
+     *                            for contact names.
+     * @param contactInfo The contact info which was looked up in the contact cache.
+     * @param call The call to generate a title for.
+     * @return The content title.
+     */
+    private @Nullable String getContentTitle(Context context,
+            @Nullable ContactsPreferences contactsPreferences,
+            ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
+
+        if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) &&
+                !call.getDetails()
+                        .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+            return context.getResources().getString(R.string.card_title_conf_call);
+        }
+
+        String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary,
+                contactInfo.nameAlternative, contactsPreferences);
+        if (TextUtils.isEmpty(preferredName)) {
+            return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance()
+                    .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+        }
+        return preferredName;
+    }
+
+    /**
+     * Gets a "person reference" for a notification, used by the system to determine whether the
+     * notification should be allowed past notification interruption filters.
+     *
+     * @param contactInfo The contact info from cache.
+     * @param call The call.
+     * @return the person reference.
+     */
+    private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo,
+            Call call) {
+
+        String number = TelecomCallUtil.getNumber(call);
+        // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+        // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+        // NotificationManager using it.
+        if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+            return contactInfo.lookupUri.toString();
+        } else if (!TextUtils.isEmpty(number)) {
+            return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString();
+        }
+        return "";
+    }
+}
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 0109d7e..38507ee 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -41,6 +41,7 @@
 import android.view.WindowManager;
 
 import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.CallSdkCompat;
 import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
 import com.android.contacts.common.interactions.TouchPointManager;
@@ -109,9 +110,11 @@
 
     private AudioModeProvider mAudioModeProvider;
     private StatusBarNotifier mStatusBarNotifier;
+    private ExternalCallNotifier mExternalCallNotifier;
     private ContactInfoCache mContactInfoCache;
     private Context mContext;
     private CallList mCallList;
+    private ExternalCallList mExternalCallList;
     private InCallActivity mInCallActivity;
     private InCallState mInCallState = InCallState.NO_CALLS;
     private ProximitySensor mProximitySensor;
@@ -299,8 +302,10 @@
 
     public void setUp(Context context,
             CallList callList,
+            ExternalCallList externalCallList,
             AudioModeProvider audioModeProvider,
             StatusBarNotifier statusBarNotifier,
+            ExternalCallNotifier externalCallNotifier,
             ContactInfoCache contactInfoCache,
             ProximitySensor proximitySensor) {
         if (mServiceConnected) {
@@ -318,6 +323,7 @@
         mContactInfoCache = contactInfoCache;
 
         mStatusBarNotifier = statusBarNotifier;
+        mExternalCallNotifier = externalCallNotifier;
         addListener(mStatusBarNotifier);
 
         mAudioModeProvider = audioModeProvider;
@@ -329,6 +335,8 @@
         addInCallUiListener(mAnswerPresenter);
 
         mCallList = callList;
+        mExternalCallList = externalCallList;
+        externalCallList.addExternalCallListener(mExternalCallNotifier);
 
         // This only gets called by the service so this is okay.
         mServiceConnected = true;
@@ -501,7 +509,12 @@
         if (shouldAttemptBlocking(call)) {
             maybeBlockCall(call);
         } else {
-            mCallList.onCallAdded(call);
+            if (call.getDetails()
+                    .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+                mExternalCallList.onCallAdded(call);
+            } else {
+                mCallList.onCallAdded(call);
+            }
         }
 
         // Since a call has been added we are no longer waiting for Telecom to send us a call.
@@ -590,8 +603,13 @@
     }
 
     public void onCallRemoved(android.telecom.Call call) {
-        mCallList.onCallRemoved(call);
-        call.unregisterCallback(mCallCallback);
+        if (call.getDetails()
+                .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+            mExternalCallList.onCallRemoved(call);
+        } else {
+            mCallList.onCallRemoved(call);
+            call.unregisterCallback(mCallCallback);
+        }
     }
 
     public void onCanAddCallChanged(boolean canAddCall) {
@@ -1506,6 +1524,9 @@
             if (mStatusBarNotifier != null) {
                 removeListener(mStatusBarNotifier);
             }
+            if (mExternalCallNotifier != null && mExternalCallList != null) {
+                mExternalCallList.removeExternalCallListener(mExternalCallNotifier);
+            }
             mStatusBarNotifier = null;
 
             if (mCallList != null) {
@@ -1808,6 +1829,10 @@
         return mAnswerPresenter;
     }
 
+    ExternalCallNotifier getExternalCallNotifier() {
+        return mExternalCallNotifier;
+    }
+
     /**
      * Private constructor. Must use getInstance() to get this singleton.
      */
diff --git a/InCallUI/src/com/android/incallui/InCallServiceImpl.java b/InCallUI/src/com/android/incallui/InCallServiceImpl.java
index 8693697..1414bc5 100644
--- a/InCallUI/src/com/android/incallui/InCallServiceImpl.java
+++ b/InCallUI/src/com/android/incallui/InCallServiceImpl.java
@@ -64,8 +64,10 @@
         InCallPresenter.getInstance().setUp(
                 getApplicationContext(),
                 CallList.getInstance(),
+                new ExternalCallList(),
                 AudioModeProvider.getInstance(),
                 new StatusBarNotifier(context, contactInfoCache),
+                new ExternalCallNotifier(context, contactInfoCache),
                 contactInfoCache,
                 new ProximitySensor(
                         context,
diff --git a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java
index 2543b78..27f7115 100644
--- a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java
@@ -45,6 +45,10 @@
             "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST";
     public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST =
             "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST";
+    public static final String ACTION_PULL_EXTERNAL_CALL =
+            "com.android.incallui.ACTION_PULL_EXTERNAL_CALL";
+    public static final String EXTRA_NOTIFICATION_ID =
+            "com.android.incallui.extra.EXTRA_NOTIFICATION_ID";
 
     @Override
     public void onReceive(Context context, Intent intent) {
@@ -68,6 +72,10 @@
                     VideoProfile.STATE_BIDIRECTIONAL, context);
         } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) {
             InCallPresenter.getInstance().declineUpgradeRequest(context);
+        } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) {
+            int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+            InCallPresenter.getInstance().getExternalCallNotifier()
+                    .pullExternalCall(notificationId);
         }
     }
 
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
index 7d212aa..a616203 100644
--- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java
+++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
@@ -247,8 +247,11 @@
                 getContentString(call, contactInfo.userType);
         final String contentTitle = getContentTitle(contactInfo, call);
 
+        final boolean isVideoUpgradeRequest = call.getSessionModificationState()
+                == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
         final int notificationType;
-        if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING) {
+        if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING
+                || isVideoUpgradeRequest) {
             notificationType = NOTIFICATION_INCOMING_CALL;
         } else {
             notificationType = NOTIFICATION_IN_CALL;
@@ -301,8 +304,6 @@
         builder.setLargeIcon(largeIcon);
         builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
 
-        final boolean isVideoUpgradeRequest = call.getSessionModificationState()
-                == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
         if (isVideoUpgradeRequest) {
             builder.setUsesChronometer(false);
             addDismissUpgradeRequestAction(builder);
diff --git a/InCallUI/src/com/android/incallui/VideoCallFragment.java b/InCallUI/src/com/android/incallui/VideoCallFragment.java
index cb8c644..2e65f81 100644
--- a/InCallUI/src/com/android/incallui/VideoCallFragment.java
+++ b/InCallUI/src/com/android/incallui/VideoCallFragment.java
@@ -499,6 +499,7 @@
     public void onPause() {
         super.onPause();
         Log.d(this, "onPause:");
+        getPresenter().cancelAutoFullScreen();
     }
 
     @Override
@@ -549,6 +550,7 @@
      * Hides and shows the incoming video view and changes the outgoing video view's state based on
      * whether outgoing view is enabled or not.
      */
+    @Override
     public void showVideoViews(boolean previewPaused, boolean showIncoming) {
         inflateVideoUi(true);
 
@@ -567,6 +569,7 @@
     /**
      * Hide all video views.
      */
+    @Override
     public void hideVideoUi() {
         inflateVideoUi(false);
     }
diff --git a/InCallUI/src/com/android/incallui/VideoCallPresenter.java b/InCallUI/src/com/android/incallui/VideoCallPresenter.java
index 9a33d80..7617608 100644
--- a/InCallUI/src/com/android/incallui/VideoCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/VideoCallPresenter.java
@@ -81,7 +81,9 @@
     private Runnable mAutoFullscreenRunnable =  new Runnable() {
         @Override
         public void run() {
-            if (mAutoFullScreenPending && !InCallPresenter.getInstance().isDialpadVisible()) {
+            if (mAutoFullScreenPending && !InCallPresenter.getInstance().isDialpadVisible()
+                    && mIsVideoMode) {
+
                 Log.v(this, "Automatically entering fullscreen mode.");
                 InCallPresenter.getInstance().setFullScreen(true);
                 mAutoFullScreenPending = false;
@@ -258,6 +260,8 @@
             return;
         }
 
+        cancelAutoFullScreen();
+
         InCallPresenter.getInstance().removeListener(this);
         InCallPresenter.getInstance().removeDetailsListener(this);
         InCallPresenter.getInstance().removeIncomingCallListener(this);
@@ -495,7 +499,7 @@
         updateCameraSelection(call);
 
         if (isVideoCall) {
-            enterVideoMode(call);
+            adjustVideoMode(call);
         } else if (isVideoMode()) {
             exitVideoMode();
         }
@@ -555,7 +559,7 @@
             Log.d(this, "onPrimaryCallChanged: Entering video mode...");
 
             updateCameraSelection(newPrimaryCall);
-            enterVideoMode(newPrimaryCall);
+            adjustVideoMode(newPrimaryCall);
         }
     }
 
@@ -630,7 +634,7 @@
      * Handles a change to the video call. Sets the surfaces on the previous call to null and sets
      * the surfaces on the new video call accordingly.
      *
-     * @param videoCall The new video call.
+     * @param call The new video call.
      */
     private void changeVideoCall(Call call) {
         final VideoCall videoCall = call.getTelecomCall().getVideoCall();
@@ -651,13 +655,13 @@
         }
 
         if (VideoUtils.isVideoCall(call) && hasChanged) {
-            enterVideoMode(call);
+            adjustVideoMode(call);
         }
     }
 
     private static boolean isCameraRequired(int videoState) {
-        return VideoProfile.isBidirectional(videoState) ||
-                VideoProfile.isTransmissionEnabled(videoState);
+        return VideoProfile.isBidirectional(videoState)
+                || VideoProfile.isTransmissionEnabled(videoState);
     }
 
     private boolean isCameraRequired() {
@@ -665,14 +669,16 @@
     }
 
     /**
-     * Enters video mode by showing the video surfaces and making other adjustments (eg. audio).
+     * Adjusts the current video mode by setting up the preview and display surfaces as necessary.
+     * Expected to be called whenever the video state associated with a call changes (e.g. a user
+     * turns their camera on or off) to ensure the correct surfaces are shown/hidden.
      * TODO(vt): Need to adjust size and orientation of preview surface here.
      */
-    private void enterVideoMode(Call call) {
+    private void adjustVideoMode(Call call) {
         VideoCall videoCall = call.getVideoCall();
         int newVideoState = call.getVideoState();
 
-        Log.d(this, "enterVideoMode videoCall= " + videoCall + " videoState: " + newVideoState);
+        Log.d(this, "adjustVideoMode videoCall= " + videoCall + " videoState: " + newVideoState);
         VideoCallUi ui = getUi();
         if (ui == null) {
             Log.e(this, "Error VideoCallUi is null so returning");
@@ -692,16 +698,15 @@
             videoCall.setDeviceOrientation(mDeviceOrientation);
             enableCamera(videoCall, isCameraRequired(newVideoState));
         }
+        int previousVideoState = mCurrentVideoState;
         mCurrentVideoState = newVideoState;
-
         mIsVideoMode = true;
 
-        maybeAutoEnterFullscreen(call);
-    }
-
-    private static boolean isSpeakerEnabledForVideoCalls() {
-        // TODO: Make this a carrier configurable setting. For now this is always true. b/20090407
-        return true;
+        // adjustVideoMode may be called if we are already in a 1-way video state.  In this case
+        // we do not want to trigger auto-fullscreen mode.
+        if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+            maybeAutoEnterFullscreen(call);
+        }
     }
 
     private void enableCamera(VideoCall videoCall, boolean isCameraRequired) {
@@ -1068,6 +1073,8 @@
      * 2. Call is not active
      * 3. Call is not video call
      * 4. Already in fullscreen mode
+     * 5. The current video state is not bi-directional (if the remote party stops transmitting,
+     *    the user's contact photo would dominate in fullscreen mode).
      *
      * @param call The current call.
      */
@@ -1079,7 +1086,8 @@
         if (call == null || (
                 call != null && (call.getState() != Call.State.ACTIVE ||
                         !VideoUtils.isVideoCall(call)) ||
-                        InCallPresenter.getInstance().isFullscreen())) {
+                        InCallPresenter.getInstance().isFullscreen()) ||
+                        !VideoUtils.isBidirectionalVideoCall(call)) {
             // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
             cancelAutoFullScreen();
             return;
@@ -1106,10 +1114,6 @@
         mAutoFullScreenPending = false;
     }
 
-    private static boolean isAudioRouteEnabled(int audioRoute, int audioRouteMask) {
-        return ((audioRoute & audioRouteMask) != 0);
-    }
-
     private static void updateCameraSelection(Call call) {
         Log.d(TAG, "updateCameraSelection: call=" + call);
         Log.d(TAG, "updateCameraSelection: call=" + toSimpleString(call));
diff --git a/InCallUI/src/com/android/incallui/VideoUtils.java b/InCallUI/src/com/android/incallui/VideoUtils.java
index 8641d60..a2eb8bc 100644
--- a/InCallUI/src/com/android/incallui/VideoUtils.java
+++ b/InCallUI/src/com/android/incallui/VideoUtils.java
@@ -45,6 +45,14 @@
         return VideoProfile.isBidirectional(call.getVideoState());
     }
 
+    public static boolean isTransmissionEnabled(Call call) {
+        if (!CompatUtils.isVideoCompatible()) {
+            return false;
+        }
+
+        return VideoProfile.isTransmissionEnabled(call.getVideoState());
+    }
+
     public static boolean isIncomingVideoCall(Call call) {
         if (!VideoUtils.isVideoCall(call)) {
             return false;
diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java
new file mode 100644
index 0000000..070bdf5
--- /dev/null
+++ b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.*;
+import android.telecom.Call;
+import android.test.AndroidTestCase;
+
+import java.lang.reflect.Constructor;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class ExternalCallListTest extends AndroidTestCase {
+
+    private static class Listener implements ExternalCallList.ExternalCallListener {
+        private CountDownLatch mCallAddedLatch = new CountDownLatch(1);
+        private CountDownLatch mCallRemovedLatch = new CountDownLatch(1);
+        private CountDownLatch mCallUpdatedLatch = new CountDownLatch(1);
+
+        @Override
+        public void onExternalCallAdded(Call call) {
+            mCallAddedLatch.countDown();
+        }
+
+        @Override
+        public void onExternalCallRemoved(Call call) {
+            mCallRemovedLatch.countDown();
+        }
+
+        @Override
+        public void onExternalCallUpdated(Call call) {
+            mCallUpdatedLatch.countDown();
+        }
+
+        public boolean awaitCallAdded() {
+            try {
+                return mCallAddedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                return false;
+            }
+        }
+
+        public boolean awaitCallRemoved() {
+            try {
+                return mCallRemovedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                return false;
+            }
+        }
+
+        public boolean awaitCallUpdated() {
+            try {
+                return mCallUpdatedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                return false;
+            }
+        }
+    }
+
+    private static final int WAIT_TIMEOUT_MILLIS = 5000;
+
+    private ExternalCallList mExternalCallList = new ExternalCallList();
+    private Listener mExternalCallListener = new Listener();
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mExternalCallList.addExternalCallListener(mExternalCallListener);
+    }
+
+    public void testAddCallSuccess() {
+        TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL);
+        mExternalCallList.onCallAdded(call.getCall());
+        assertTrue(mExternalCallListener.awaitCallAdded());
+    }
+
+    public void testAddCallFail() {
+        TestTelecomCall call = getTestCall(0 /* no properties */);
+        try {
+            mExternalCallList.onCallAdded(call.getCall());
+            fail();
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testUpdateCall() {
+        TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL);
+        mExternalCallList.onCallAdded(call.getCall());
+        assertTrue(mExternalCallListener.awaitCallAdded());
+
+        call.forceDetailsUpdate();
+        assertTrue(mExternalCallListener.awaitCallUpdated());
+    }
+
+    public void testRemoveCall() {
+        TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL);
+        mExternalCallList.onCallAdded(call.getCall());
+        assertTrue(mExternalCallListener.awaitCallAdded());
+
+        mExternalCallList.onCallRemoved(call.getCall());
+        assertTrue(mExternalCallListener.awaitCallRemoved());
+    }
+
+    private TestTelecomCall getTestCall(int properties) {
+        TestTelecomCall testCall = TestTelecomCall.createInstance(
+                "1",
+                Uri.parse("tel:650-555-1212"), /* handle */
+                TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */
+                "Joe", /* callerDisplayName */
+                TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */
+                new PhoneAccountHandle(new ComponentName("test", "class"),
+                        "handle"), /* accountHandle */
+                Call.Details.CAPABILITY_CAN_PULL_CALL, /* capabilities */
+                properties, /* properties */
+                null, /* disconnectCause */
+                0, /* connectTimeMillis */
+                null, /* GatewayInfo */
+                VideoProfile.STATE_AUDIO_ONLY, /* videoState */
+                null, /* statusHints */
+                null, /* extras */
+                null /* intentExtras */);
+        return testCall;
+    }
+}
diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java
new file mode 100644
index 0000000..e57efef
--- /dev/null
+++ b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2016 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 static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.telecom.*;
+import android.telecom.Call;
+import android.telephony.TelephonyManager;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContext;
+
+/**
+ * Unit tests for {@link ExternalCallNotifier}.
+ */
+public class ExternalCallNotifierTest extends AndroidTestCase {
+    private static final int TIMEOUT_MILLIS = 5000;
+    private static final String NAME_PRIMARY = "Full Name";
+    private static final String NAME_ALTERNATIVE = "Name, Full";
+    private static final String LOCATION = "US";
+    private static final String NUMBER = "6505551212";
+
+    @Mock private ContactsPreferences mContactsPreferences;
+    @Mock private NotificationManager mNotificationManager;
+    @Mock private MockContext mMockContext;
+    @Mock private Resources mResources;
+    @Mock private StatusBarNotifier mStatusBarNotifier;
+    @Mock private ContactInfoCache mContactInfoCache;
+    @Mock private TelecomManager mTelecomManager;
+    @Mock private TelephonyManager mTelephonyManager;
+    @Mock private ProximitySensor mProximitySensor;
+    @Mock private CallList mCallList;
+    private InCallPresenter mInCallPresenter;
+    private ExternalCallNotifier mExternalCallNotifier;
+    private ContactInfoCache.ContactCacheEntry mContactInfo;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+
+        when(mContactsPreferences.getDisplayOrder())
+                .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+
+        // Setup the mock context to return mocks for some of the needed services; the notification
+        // service is especially important as we want to be able to intercept calls into it and
+        // validate the notifcations.
+        when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+                .thenReturn(mNotificationManager);
+        when(mMockContext.getSystemService(eq(Context.TELECOM_SERVICE)))
+                .thenReturn(mTelecomManager);
+        when(mMockContext.getSystemService(eq(Context.TELEPHONY_SERVICE)))
+                .thenReturn(mTelephonyManager);
+
+        // These aspects of the context are used by the notification builder to build the actual
+        // notification; we will rely on the actual implementations of these.
+        when(mMockContext.getPackageManager()).thenReturn(mContext.getPackageManager());
+        when(mMockContext.getResources()).thenReturn(mContext.getResources());
+        when(mMockContext.getApplicationInfo()).thenReturn(mContext.getApplicationInfo());
+        when(mMockContext.getContentResolver()).thenReturn(mContext.getContentResolver());
+        when(mMockContext.getPackageName()).thenReturn(mContext.getPackageName());
+
+        ContactsPreferencesFactory.setTestInstance(null);
+        mExternalCallNotifier = new ExternalCallNotifier(mMockContext, mContactInfoCache);
+
+        // We don't directly use the InCallPresenter in the test, or even in ExternalCallNotifier
+        // itself.  However, ExternalCallNotifier needs to make instances of
+        // com.android.incallui.Call for the purpose of performing contact cache lookups.  The
+        // Call class depends on the static InCallPresenter for a number of things, so we need to
+        // set it up here to prevent crashes.
+        mInCallPresenter = InCallPresenter.getInstance();
+        mInCallPresenter.setUp(mMockContext, mCallList, new ExternalCallList(),
+                null, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache,
+                mProximitySensor);
+
+        // Unlocked all contact info is available
+        mContactInfo = new ContactInfoCache.ContactCacheEntry();
+        mContactInfo.namePrimary = NAME_PRIMARY;
+        mContactInfo.nameAlternative = NAME_ALTERNATIVE;
+        mContactInfo.location = LOCATION;
+        mContactInfo.number = NUMBER;
+
+        // Given the mock ContactInfoCache cache, we need to mock out what happens when the
+        // ExternalCallNotifier calls into the contact info cache to do a lookup.  We will always
+        // return mock info stored in mContactInfo.
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] args = invocation.getArguments();
+                com.android.incallui.Call call = (com.android.incallui.Call) args[0];
+                ContactInfoCache.ContactInfoCacheCallback callback
+                        = (ContactInfoCache.ContactInfoCacheCallback) args[2];
+                callback.onContactInfoComplete(call.getId(), mContactInfo);
+                return null;
+            }
+        }).when(mContactInfoCache).findInfo(any(com.android.incallui.Call.class), anyBoolean(),
+                any(ContactInfoCache.ContactInfoCacheCallback.class));
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        ContactsPreferencesFactory.setTestInstance(null);
+        mInCallPresenter.tearDown();
+    }
+
+    public void testPostNonPullable() {
+        TestTelecomCall call = getTestCall(false);
+        mExternalCallNotifier.onExternalCallAdded(call.getCall());
+        Notification notification = verifyNotificationPosted();
+        assertNull(notification.actions);
+    }
+
+    public void testPostPullable() {
+        TestTelecomCall call = getTestCall(true);
+        mExternalCallNotifier.onExternalCallAdded(call.getCall());
+        Notification notification = verifyNotificationPosted();
+        assertEquals(1, notification.actions.length);
+    }
+
+    public void testNotificationDismissed() {
+        TestTelecomCall call = getTestCall(false);
+        mExternalCallNotifier.onExternalCallAdded(call.getCall());
+        verifyNotificationPosted();
+
+        mExternalCallNotifier.onExternalCallRemoved(call.getCall());
+        verify(mNotificationManager, timeout(TIMEOUT_MILLIS)).cancel(eq("EXTERNAL_CALL"), eq(0));
+    }
+
+    public void testNotificationUpdated() {
+        TestTelecomCall call = getTestCall(false);
+        mExternalCallNotifier.onExternalCallAdded(call.getCall());
+        verifyNotificationPosted();
+
+        call.setCapabilities(android.telecom.Call.Details.CAPABILITY_CAN_PULL_CALL);
+        mExternalCallNotifier.onExternalCallUpdated(call.getCall());
+
+        ArgumentCaptor<Notification> notificationCaptor =
+                ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager, timeout(TIMEOUT_MILLIS).times(2))
+                .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture());
+        Notification notification1 = notificationCaptor.getAllValues().get(0);
+        assertNull(notification1.actions);
+        Notification notification2 = notificationCaptor.getAllValues().get(1);
+        assertEquals(1, notification2.actions.length);
+    }
+
+    private Notification verifyNotificationPosted() {
+        ArgumentCaptor<Notification> notificationCaptor =
+                ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager, timeout(TIMEOUT_MILLIS))
+                .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture());
+        return notificationCaptor.getValue();
+    }
+
+    private TestTelecomCall getTestCall(boolean canPull) {
+        TestTelecomCall testCall = TestTelecomCall.createInstance(
+                "1",
+                Uri.parse("tel:650-555-1212"), /* handle */
+                TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */
+                "Joe", /* callerDisplayName */
+                TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */
+                new PhoneAccountHandle(new ComponentName("test", "class"),
+                        "handle"), /* accountHandle */
+                canPull ? android.telecom.Call.Details.CAPABILITY_CAN_PULL_CALL : 0, /* capabilities */
+                Call.Details.PROPERTY_IS_EXTERNAL_CALL, /* properties */
+                null, /* disconnectCause */
+                0, /* connectTimeMillis */
+                null, /* GatewayInfo */
+                VideoProfile.STATE_AUDIO_ONLY, /* videoState */
+                null, /* statusHints */
+                null, /* extras */
+                null /* intentExtras */);
+        return testCall;
+    }
+}
diff --git a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java b/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java
index ed8d622..f0f08ab 100644
--- a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java
+++ b/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java
@@ -39,6 +39,7 @@
     @Mock private InCallActivity mInCallActivity;
     @Mock private AudioModeProvider mAudioModeProvider;
     @Mock private StatusBarNotifier mStatusBarNotifier;
+    @Mock private ExternalCallNotifier mExternalCallNotifier;
     @Mock private ContactInfoCache mContactInfoCache;
     @Mock private ProximitySensor mProximitySensor;
 
@@ -57,8 +58,9 @@
         when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
 
         mInCallPresenter = InCallPresenter.getInstance();
-        mInCallPresenter.setUp(mContext, mCallList.getCallList(), mAudioModeProvider,
-                mStatusBarNotifier, mContactInfoCache, mProximitySensor);
+        mInCallPresenter.setUp(mContext, mCallList.getCallList(), new ExternalCallList(),
+                mAudioModeProvider, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache,
+                mProximitySensor);
     }
 
     @Override
diff --git a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java
new file mode 100644
index 0000000..48ac6e1
--- /dev/null
+++ b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 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.common.base.Preconditions;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Wrapper class which uses reflection to create instances of {@link android.telecom.Call} for use
+ * with unit testing.  Since {@link android.telecom.Call} is final, it cannot be mocked using
+ * mockito, and since all setter methods are hidden, it is necessary to use reflection.  In the
+ * future, it would be desirable to replace this if a different mocking solution is used.
+ */
+public class TestTelecomCall {
+
+    private android.telecom.Call mCall;
+
+    public static @Nullable TestTelecomCall createInstance(String callId,
+            Uri handle,
+            int handlePresentation,
+            String callerDisplayName,
+            int callerDisplayNamePresentation,
+            PhoneAccountHandle accountHandle,
+            int capabilities,
+            int properties,
+            DisconnectCause disconnectCause,
+            long connectTimeMillis,
+            GatewayInfo gatewayInfo,
+            int videoState,
+            StatusHints statusHints,
+            Bundle extras,
+            Bundle intentExtras) {
+
+        try {
+            // Phone and InCall adapter are @hide, so we cannot refer to them directly.
+            Class<?> phoneClass = Class.forName("android.telecom.Phone");
+            Class<?> incallAdapterClass = Class.forName("android.telecom.InCallAdapter");
+            Class<?> callClass = android.telecom.Call.class;
+            Constructor<?> cons = callClass
+                    .getDeclaredConstructor(phoneClass, String.class, incallAdapterClass);
+            cons.setAccessible(true);
+
+            android.telecom.Call call = (android.telecom.Call) cons.newInstance(null, callId, null);
+
+            // Create an instance of the call details.
+            Class<?> callDetailsClass = android.telecom.Call.Details.class;
+            Constructor<?> detailsCons = callDetailsClass.getDeclaredConstructor(
+                    String.class, /* telecomCallId */
+                    Uri.class, /* handle */
+                    int.class, /* handlePresentation */
+                    String.class, /* callerDisplayName */
+                    int.class, /* callerDisplayNamePresentation */
+                    PhoneAccountHandle.class, /* accountHandle */
+                    int.class, /* capabilities */
+                    int.class, /* properties */
+                    DisconnectCause.class, /* disconnectCause */
+                    long.class, /* connectTimeMillis */
+                    GatewayInfo.class, /* gatewayInfo */
+                    int.class, /* videoState */
+                    StatusHints.class, /* statusHints */
+                    Bundle.class, /* extras */
+                    Bundle.class /* intentExtras */);
+            detailsCons.setAccessible(true);
+
+            android.telecom.Call.Details details = (android.telecom.Call.Details)
+                    detailsCons.newInstance(callId, handle, handlePresentation, callerDisplayName,
+                            callerDisplayNamePresentation, accountHandle, capabilities, properties,
+                            disconnectCause, connectTimeMillis, gatewayInfo, videoState,
+                            statusHints,
+                            extras, intentExtras);
+
+            // Finally, set this as the details of the call.
+            Field detailsField = call.getClass().getDeclaredField("mDetails");
+            detailsField.setAccessible(true);
+            detailsField.set(call, details);
+
+            return new TestTelecomCall(call);
+        } catch (NoSuchMethodException nsm) {
+            return null;
+        } catch (ClassNotFoundException cnf) {
+            return null;
+        } catch (IllegalAccessException e) {
+            return null;
+        } catch (InstantiationException e) {
+            return null;
+        } catch (InvocationTargetException e) {
+            return null;
+        } catch (NoSuchFieldException e) {
+            return null;
+        }
+    }
+
+    private TestTelecomCall(android.telecom.Call call) {
+        mCall = call;
+    }
+
+    public android.telecom.Call getCall() {
+        return mCall;
+    }
+
+    public void forceDetailsUpdate() {
+        Preconditions.checkNotNull(mCall);
+
+        try {
+            Method method = mCall.getClass().getDeclaredMethod("fireDetailsChanged",
+                    android.telecom.Call.Details.class);
+            method.setAccessible(true);
+            method.invoke(mCall, mCall.getDetails());
+        } catch (NoSuchMethodException e) {
+            Log.e(this, "forceDetailsUpdate", e);
+        } catch (InvocationTargetException e) {
+            Log.e(this, "forceDetailsUpdate", e);
+        } catch (IllegalAccessException e) {
+            Log.e(this, "forceDetailsUpdate", e);
+        }
+    }
+
+    public void setCapabilities(int capabilities) {
+        Preconditions.checkNotNull(mCall);
+        try {
+            Field field = mCall.getDetails().getClass().getDeclaredField("mCallCapabilities");
+            field.setAccessible(true);
+            field.set(mCall.getDetails(), capabilities);
+        } catch (IllegalAccessException e) {
+            Log.e(this, "setProperties", e);
+        } catch (NoSuchFieldException e) {
+            Log.e(this, "setProperties", e);
+        }
+    }
+
+    public void setCall(android.telecom.Call call) {
+        mCall = call;
+    }
+}