Merge "Transplant Logging to android.telecom" am: 6817ba9be2 am: e0a9727add
am: e7d6572db8

Change-Id: I35ba6f1e78f8f23a147d9af54c837d478ed05505
diff --git a/Android.mk b/Android.mk
index 4785b24..79ef194 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,9 +5,13 @@
 
 LOCAL_JAVA_LIBRARIES := telephony-common
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-proto-files-under, proto)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
+LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/proto/
+LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors
+
 LOCAL_PACKAGE_NAME := Telecom
 
 LOCAL_CERTIFICATE := platform
diff --git a/proto/telecom.proto b/proto/telecom.proto
new file mode 100644
index 0000000..06dd394
--- /dev/null
+++ b/proto/telecom.proto
@@ -0,0 +1,280 @@
+syntax = "proto2";
+
+package com.android.server.telecom;
+
+option java_package = "com.android.server.telecom";
+option java_outer_classname = "TelecomLogClass";
+
+// The information about the telecom events.
+message TelecomLog {
+
+  // Information about each call.
+  repeated CallLog call_logs = 1;
+
+  // Timing information for the logging sessions
+  repeated LogSessionTiming session_timings = 2;
+}
+
+message LogSessionTiming {
+  enum SessionEntryPoint {
+    ICA_ANSWER_CALL = 1;
+    ICA_REJECT_CALL = 2;
+    ICA_DISCONNECT_CALL = 3;
+    ICA_HOLD_CALL = 4;
+    ICA_UNHOLD_CALL = 5;
+    ICA_MUTE = 6;
+    ICA_SET_AUDIO_ROUTE = 7;
+    ICA_CONFERENCE = 8;
+
+    CSW_HANDLE_CREATE_CONNECTION_COMPLETE = 100;
+    CSW_SET_ACTIVE = 101;
+    CSW_SET_RINGING = 102;
+    CSW_SET_DIALING = 103;
+    CSW_SET_DISCONNECTED = 104;
+    CSW_SET_ON_HOLD = 105;
+    CSW_REMOVE_CALL = 106;
+    CSW_SET_IS_CONFERENCED = 107;
+    CSW_ADD_CONFERENCE_CALL = 108;
+  }
+
+  // The entry point into Telecom code that this session tracks.
+  optional SessionEntryPoint sessionEntryPoint = 1;
+  // The time it took for this session to finish.
+  optional int64 time_millis = 2;
+}
+
+message Event {
+  // From android.telecom.ParcelableAnalytics
+  enum EventName {
+    SET_SELECT_PHONE_ACCOUNT = 0;
+    SET_ACTIVE = 1;
+    SET_DISCONNECTED = 2;
+    START_CONNECTION = 3;
+    SET_DIALING = 4;
+    BIND_CS = 5;
+    CS_BOUND = 6;
+    REQUEST_ACCEPT = 7;
+    REQUEST_REJECT = 8;
+
+    SCREENING_SENT = 100;
+    SCREENING_COMPLETED = 101;
+    DIRECT_TO_VM_INITIATED = 102;
+    DIRECT_TO_VM_FINISHED = 103;
+    BLOCK_CHECK_INITIATED = 104;
+    BLOCK_CHECK_FINISHED = 105;
+    FILTERING_INITIATED = 106;
+    FILTERING_COMPLETED = 107;
+    FILTERING_TIMED_OUT = 108;
+
+    SKIP_RINGING = 200;
+    SILENCE = 201;
+    MUTE = 202;
+    UNMUTE = 203;
+    AUDIO_ROUTE_BT = 204;
+    AUDIO_ROUTE_EARPIECE = 205;
+    AUDIO_ROUTE_HEADSET = 206;
+    AUDIO_ROUTE_SPEAKER = 207;
+
+    CONFERENCE_WITH = 300;
+    SPLIT_CONFERENCE = 301;
+    SET_PARENT = 302;
+
+    REQUEST_HOLD = 400;
+    REQUEST_UNHOLD = 401;
+    REMOTELY_HELD = 402;
+    REMOTELY_UNHELD = 403;
+    SET_HOLD = 404;
+    SWAP = 405;
+
+    REQUEST_PULL = 500;
+  }
+
+  // The ID of the event.
+  optional EventName event_name = 1;
+
+  // The elapsed time since the last event, rounded to one significant digit.
+  // If the event is the first, this will be negative.
+  optional int64 time_since_last_event_millis = 2;
+}
+
+message VideoEvent {
+  // From android.telecom.ParcelableCallAnalytics
+  enum VideoEventName {
+    SEND_LOCAL_SESSION_MODIFY_REQUEST = 0;
+    SEND_LOCAL_SESSION_MODIFY_RESPONSE = 1;
+    RECEIVE_REMOTE_SESSION_MODIFY_REQUEST = 2;
+    RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE = 3;
+  }
+
+  // From android.telecom.VideoProfile
+  enum VideoState {
+     STATE_AUDIO_ONLY = 0;
+     STATE_TX_ENABLED = 1;
+     STATE_RX_ENABLED = 2;
+     STATE_BIDIRECTIONAL = 3;
+     STATE_PAUSED = 4;
+  }
+
+  // The ID of the event.
+  optional VideoEventName event_name = 1;
+
+  // The elapsed time since the last event, rounded to one significant digit.
+  // If the event is the first, this will be negative.
+  optional int64 time_since_last_event_millis = 2;
+
+  // The video state
+  optional int32 video_state = 3;
+}
+
+message EventTimingEntry {
+  enum EventTimingName {
+    ACCEPT_TIMING = 0;
+    REJECT_TIMING = 1;
+    DISCONNECT_TIMING = 2;
+    HOLD_TIMING = 3;
+    UNHOLD_TIMING = 4;
+    OUTGOING_TIME_TO_DIALING_TIMING = 5;
+    BIND_CS_TIMING = 6;
+    SCREENING_COMPLETED_TIMING = 7;
+    DIRECT_TO_VM_FINISHED_TIMING = 8;
+    BLOCK_CHECK_FINISHED_TIMING = 9;
+    FILTERING_COMPLETED_TIMING = 10;
+    FILTERING_TIMED_OUT_TIMING = 11;
+  }
+
+  // The name of the event timing.
+  optional EventTimingName timing_name = 1;
+
+  // The number of milliseconds that this event pair took.
+  optional int64 time_millis = 2;
+}
+
+message InCallServiceInfo {
+  // Keep this up-to-date with com.android.server.telecom.InCallController.
+  enum InCallServiceType {
+    IN_CALL_SERVICE_TYPE_INVALID = 0;
+    IN_CALL_SERVICE_TYPE_DIALER_UI = 1;
+    IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
+    IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
+    IN_CALL_SERVICE_TYPE_NON_UI = 4;
+  }
+
+  // The shortened component name of the in-call service.
+  optional string in_call_service_name = 1;
+
+  // The type of the in-call service
+  optional InCallServiceType in_call_service_type = 2;
+}
+
+// Information about each call.
+message CallLog {
+
+  // Information on call-types.
+  enum CallType {
+
+    // Call type is not known.
+    CALLTYPE_UNKNOWN = 0;
+
+    // Incoming call.
+    CALLTYPE_INCOMING = 1;
+
+    // Outgoing call.
+    CALLTYPE_OUTGOING = 2;
+  }
+
+  // Termination code.
+  enum CallTerminationCode {
+
+    // Disconnected because of an unknown or unspecified reason.
+    CALL_TERMINATION_CODE_UNKNOWN = 0;
+
+    // Disconnected because there was an error, such as a problem
+    // with the network.
+    CALL_TERMINATION_CODE_ERROR = 1;
+
+    // Disconnected because of a local user-initiated action,
+    // such as hanging up.
+    CALL_TERMINATION_CODE_LOCAL = 2;
+
+    // Disconnected because of a remote user-initiated action,
+    // such as the other party hanging up.
+    CALL_TERMINATION_CODE_REMOTE = 3;
+
+    // Disconnected because it has been canceled.
+    CALL_TERMINATION_CODE_CANCELED = 4;
+
+    // Disconnected because there was no response to an incoming call.
+    CALL_TERMINATION_CODE_MISSED = 5;
+
+    // Disconnected because the user rejected an incoming call.
+    CALL_TERMINATION_CODE_REJECTED = 6;
+
+    // Disconnected because the other party was busy.
+    CALL_TERMINATION_CODE_BUSY = 7;
+
+    // Disconnected because of a restriction on placing the call,
+    // such as dialing in airplane mode.
+    CALL_TERMINATION_CODE_RESTRICTED = 8;
+
+    // Disconnected for reason not described by other disconnect codes.
+    CALL_TERMINATION_CODE_OTHER = 9;
+
+    // Disconnected because the connection manager did not support the call.
+    // The call will be tried again without a connection manager.
+    CONNECTION_MANAGER_NOT_SUPPORTED = 10;
+  }
+
+  // Start time of the connection.
+  // Rounded to the nearest 5 minute interval.
+  optional int64 start_time_5min = 1;
+
+  // Duration in millis.
+  optional int64 call_duration_millis = 2;
+
+  // Call type.
+  optional CallType type  = 3;
+
+  // True if the call interrupted an in-progress call, whether it was the
+  // user dialing out during a call or an incoming call during another call.
+  optional bool is_additional_call = 4 [default = false];
+
+  // True if the call was interrupted by another call.
+  optional bool is_interrupted = 5 [default = false];
+
+  // A bitmask with bits corresponding to call technologies that were used
+  // during the call. The ones that we will record are CDMA, GSM, IMS, SIP,
+  // and third-party.
+  // See the com.android.server.telecom.Analytics.*_PHONE constants.
+  optional int32 call_technologies = 6;
+
+  // Indicates the call termination code.
+  optional CallTerminationCode call_termination_code = 7;
+
+  // A list of the package names of connection services used.
+  repeated string connection_service = 9;
+
+  // Set to true if the call was created from createCallForExistingConnection.
+  optional bool is_created_from_existing_connection = 10 [default = false];
+
+  // Set to true if its an emergency call.
+  optional bool is_emergency_call = 11 [default = false];
+
+  // A list of the events that occur during the call.
+  repeated Event call_events = 12;
+
+  // A map from the names of latency timings to the timings.
+  repeated EventTimingEntry call_timings = 13;
+
+  // Whether this call has ever been a video call
+  optional bool is_video_call = 14 [default = false];
+
+  // A list of the video events during the call.
+  repeated VideoEvent video_events = 15;
+
+  // A list of the in-call services bound during the call.
+  repeated InCallServiceInfo in_call_services = 16;
+
+  // A bitmask of the properties that were set at any point during the call.
+  // Bits are defined by android.telecom.Connection.PROPERTY_* constants.
+  optional int32 connection_properties = 17;
+}
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 75b8352..b0bcd3c 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -15,11 +15,11 @@
   -->
 
 <resources>
-    <color name="theme_color">#0288d1</color>
+    <color name="theme_color">#2a56c6</color>
 
     <color name="dialer_settings_actionbar_text_color">#ffffff</color>
     <color name="dialer_settings_actionbar_background_color">@color/theme_color</color>
-    <color name="dialer_settings_color_dark">#0277bd</color>
+    <color name="dialer_settings_color_dark">#1c3aa9</color>
 
     <color name="blocked_numbers_divider_color">#e5e5e5</color>
     <color name="blocked_numbers_butter_bar_color">#f5f5f5</color>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e46aa54..11b2d50 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -109,12 +109,13 @@
 
     <!-- Title of dialog used to comfirm whether the user intends to change the default dialer
             application [CHAR LIMIT=55]-->
-    <string name="change_default_dialer_dialog_title">Change default Dialer app?</string>
-    <!-- Text in dialog used to confirm whether or not the user intends to change the default dialer, if a different default dialer has been previously set. -->
-    <string name="change_default_dialer_with_previous_app_set_text">Use <xliff:g id="new_app">%1$s</xliff:g> instead of <xliff:g id="current_app">%2$s</xliff:g> as your default dialer app?</string>
-    <!-- Text in dialog used to confirm whether or not the user intends to change the default dialer, if a different default dialer has not been previously set. -->
-    <string name="change_default_dialer_no_previous_app_set_text">Use <xliff:g id="new_app">%s</xliff:g> as your default dialer app?</string>
-
+    <string name="change_default_dialer_dialog_title">Make <xliff:g id="new_app">%s</xliff:g> your default Phone app?</string>
+    <!-- Confirmation text that a user taps on to change the Default Phone App-->
+    <string name="change_default_dialer_dialog_affirmative">Set Default</string>
+    <!-- Cancel text that a user taps on to not change the Default Phone App-->
+    <string name="change_default_dialer_dialog_negative">Cancel</string>
+    <!-- Warning message indicating what may happen if a user allows a 3rd party app to become the default dialer.-->
+    <string name="change_default_dialer_warning_message"><xliff:g id="new_app">%s</xliff:g> will be able to place and control all aspects of calls. Only apps you trust should be set as the default Phone app.</string>
 
     <!-- Blocked numbers -->
     <string name="blocked_numbers">Blocked numbers</string>
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 1d76dfc..4456734 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -16,45 +16,172 @@
 
 package com.android.server.telecom;
 
-import android.os.Parcelable;
-import android.telecom.ParcelableCallAnalytics;
+import android.telecom.Connection;
 import android.telecom.DisconnectCause;
+import android.telecom.ParcelableCallAnalytics;
+import android.telecom.TelecomAnalytics;
+import android.util.Base64;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.CallerInfo;
 import com.android.internal.util.IndentingPrintWriter;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+
+import static android.telecom.ParcelableCallAnalytics.AnalyticsEvent;
+import static android.telecom.TelecomAnalytics.SessionTiming;
 
 /**
  * A class that collects and stores data on how calls are being made, in order to
  * aggregate these into useful statistics.
  */
 public class Analytics {
-   public static class CallInfo {
-        void setCallStartTime(long startTime) {
+    public static final String ANALYTICS_DUMPSYS_ARG = "analytics";
+    private static final String CLEAR_ANALYTICS_ARG = "clear";
+
+    public static final Map<String, Integer> sLogEventToAnalyticsEvent =
+            new HashMap<String, Integer>() {{
+                put(Log.Events.SET_SELECT_PHONE_ACCOUNT, AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT);
+                put(Log.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD);
+                put(Log.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD);
+                put(Log.Events.SWAP, AnalyticsEvent.SWAP);
+                put(Log.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING);
+                put(Log.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH);
+                put(Log.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE);
+                put(Log.Events.SET_PARENT, AnalyticsEvent.SET_PARENT);
+                put(Log.Events.MUTE, AnalyticsEvent.MUTE);
+                put(Log.Events.UNMUTE, AnalyticsEvent.UNMUTE);
+                put(Log.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT);
+                put(Log.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE);
+                put(Log.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET);
+                put(Log.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER);
+                put(Log.Events.SILENCE, AnalyticsEvent.SILENCE);
+                put(Log.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED);
+                put(Log.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED);
+                put(Log.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED);
+                put(Log.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD);
+                put(Log.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD);
+                put(Log.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL);
+                put(Log.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT);
+                put(Log.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT);
+                put(Log.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE);
+                put(Log.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED);
+                put(Log.Events.SET_HOLD, AnalyticsEvent.SET_HOLD);
+                put(Log.Events.SET_DIALING, AnalyticsEvent.SET_DIALING);
+                put(Log.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION);
+                put(Log.Events.BIND_CS, AnalyticsEvent.BIND_CS);
+                put(Log.Events.CS_BOUND, AnalyticsEvent.CS_BOUND);
+                put(Log.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT);
+                put(Log.Events.DIRECT_TO_VM_INITIATED, AnalyticsEvent.DIRECT_TO_VM_INITIATED);
+                put(Log.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED);
+                put(Log.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED);
+                put(Log.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED);
+                put(Log.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT);
+            }};
+
+    public static final Map<String, Integer> sLogSessionToSessionId =
+            new HashMap<String, Integer> () {{
+                put(Log.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL);
+                put(Log.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL);
+                put(Log.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL);
+                put(Log.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL);
+                put(Log.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL);
+                put(Log.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE);
+                put(Log.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE);
+                put(Log.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE);
+                put(Log.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
+                        SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
+                put(Log.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE);
+                put(Log.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING);
+                put(Log.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING);
+                put(Log.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED);
+                put(Log.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD);
+                put(Log.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL);
+                put(Log.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED);
+                put(Log.Sessions.CSW_ADD_CONFERENCE_CALL, SessionTiming.CSW_ADD_CONFERENCE_CALL);
+
+            }};
+
+    public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming =
+            new HashMap<String, Integer>() {{
+                put(Log.Events.Timings.ACCEPT_TIMING,
+                        ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING);
+                put(Log.Events.Timings.REJECT_TIMING,
+                        ParcelableCallAnalytics.EventTiming.REJECT_TIMING);
+                put(Log.Events.Timings.DISCONNECT_TIMING,
+                        ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING);
+                put(Log.Events.Timings.HOLD_TIMING,
+                        ParcelableCallAnalytics.EventTiming.HOLD_TIMING);
+                put(Log.Events.Timings.UNHOLD_TIMING,
+                        ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING);
+                put(Log.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
+                        ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING);
+                put(Log.Events.Timings.BIND_CS_TIMING,
+                        ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING);
+                put(Log.Events.Timings.SCREENING_COMPLETED_TIMING,
+                        ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING);
+                put(Log.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
+                        ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING);
+                put(Log.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
+                        ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING);
+                put(Log.Events.Timings.FILTERING_COMPLETED_TIMING,
+                        ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING);
+                put(Log.Events.Timings.FILTERING_TIMED_OUT_TIMING,
+                        ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING);
+            }};
+
+    public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>();
+    static {
+        for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) {
+            sSessionIdToLogSession.put(e.getValue(), e.getKey());
+        }
+    }
+
+    public static class CallInfo {
+        public void setCallStartTime(long startTime) {
         }
 
-        void setCallEndTime(long endTime) {
+        public void setCallEndTime(long endTime) {
         }
 
-        void setCallIsAdditional(boolean isAdditional) {
+        public void setCallIsAdditional(boolean isAdditional) {
         }
 
-        void setCallIsInterrupted(boolean isInterrupted) {
+        public void setCallIsInterrupted(boolean isInterrupted) {
         }
 
-        void setCallDisconnectCause(DisconnectCause disconnectCause) {
+        public void setCallDisconnectCause(DisconnectCause disconnectCause) {
         }
 
-        void addCallTechnology(int callTechnology) {
+        public void addCallTechnology(int callTechnology) {
         }
 
-        void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
+        public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
         }
 
-        void setCallConnectionService(String connectionServiceName) {
+        public void setCallConnectionService(String connectionServiceName) {
+        }
+
+        public void setCallEvents(Log.CallEventRecord records) {
+        }
+
+        public void setCallIsVideo(boolean isVideo) {
+        }
+
+        public void addVideoEvent(int eventId, int videoState) {
+        }
+
+        public void addInCallService(String serviceName, int type) {
+        }
+
+        public void addCallProperties(int properties) {
         }
     }
 
@@ -83,6 +210,15 @@
         public String connectionService;
         public boolean isEmergency = false;
 
+        public Log.CallEventRecord callEvents;
+
+        public boolean isVideo = false;
+        public List<TelecomLogClass.VideoEvent> videoEvents;
+        public List<TelecomLogClass.InCallServiceInfo> inCallServiceInfos;
+        public int callProperties = 0;
+
+        private long mTimeOfLastVideoEvent = -1;
+
         CallInfoImpl(String callId, int callDirection) {
             this.callId = callId;
             startTime = 0;
@@ -90,6 +226,8 @@
             this.callDirection = callDirection;
             callTechnologies = 0;
             connectionService = "";
+            videoEvents = new LinkedList<>();
+            inCallServiceInfos = new LinkedList<>();
         }
 
         CallInfoImpl(CallInfoImpl other) {
@@ -103,6 +241,10 @@
             this.createdFromExistingConnection = other.createdFromExistingConnection;
             this.connectionService = other.connectionService;
             this.isEmergency = other.isEmergency;
+            this.callEvents = other.callEvents;
+            this.isVideo = other.isVideo;
+            this.videoEvents = other.videoEvents;
+            this.callProperties = other.callProperties;
 
             if (other.callTerminationReason != null) {
                 this.callTerminationReason = new DisconnectCause(
@@ -167,6 +309,45 @@
         }
 
         @Override
+        public void setCallEvents(Log.CallEventRecord records) {
+            this.callEvents = records;
+        }
+
+        @Override
+        public void setCallIsVideo(boolean isVideo) {
+            this.isVideo = isVideo;
+        }
+
+        @Override
+        public void addVideoEvent(int eventId, int videoState) {
+            long timeSinceLastEvent;
+            long currentTime = System.currentTimeMillis();
+            if (mTimeOfLastVideoEvent < 0) {
+                timeSinceLastEvent = -1;
+            } else {
+                timeSinceLastEvent = roundToOneSigFig(currentTime - mTimeOfLastVideoEvent);
+            }
+            mTimeOfLastVideoEvent = currentTime;
+
+            videoEvents.add(new TelecomLogClass.VideoEvent()
+                    .setEventName(eventId)
+                    .setTimeSinceLastEventMillis(timeSinceLastEvent)
+                    .setVideoState(videoState));
+        }
+
+        @Override
+        public void addInCallService(String serviceName, int type) {
+            inCallServiceInfos.add(new TelecomLogClass.InCallServiceInfo()
+                    .setInCallServiceName(serviceName)
+                    .setInCallServiceType(type));
+        }
+
+        @Override
+        public void addCallProperties(int properties) {
+            this.callProperties |= properties;
+        }
+
+        @Override
         public String toString() {
             return "{\n"
                     + "    startTime: " + startTime + '\n'
@@ -177,28 +358,93 @@
                     + "    callTechnologies: " + getCallTechnologiesAsString() + '\n'
                     + "    callTerminationReason: " + getCallDisconnectReasonString() + '\n'
                     + "    connectionService: " + connectionService + '\n'
+                    + "    isVideoCall: " + isVideo + '\n'
+                    + "    inCallServices: " + getInCallServicesString() + '\n'
+                    + "    callProperties: " + Connection.propertiesToStringShort(callProperties)
+                    + '\n'
                     + "}\n";
         }
 
         public ParcelableCallAnalytics toParcelableAnalytics() {
+            TelecomLogClass.CallLog analyticsProto = toProto();
+            List<ParcelableCallAnalytics.AnalyticsEvent> events =
+                    Arrays.stream(analyticsProto.callEvents)
+                    .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent(
+                                callEventProto.getEventName(),
+                                callEventProto.getTimeSinceLastEventMillis())
+                    ).collect(Collectors.toList());
+
+            List<ParcelableCallAnalytics.EventTiming> timings =
+                    Arrays.stream(analyticsProto.callTimings)
+                    .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming(
+                            callTimingProto.getTimingName(),
+                            callTimingProto.getTimeMillis())
+                    ).collect(Collectors.toList());
+
+            ParcelableCallAnalytics result = new ParcelableCallAnalytics(
+                    // rounds down to nearest 5 minute mark
+                    analyticsProto.getStartTime5Min(),
+                    analyticsProto.getCallDurationMillis(),
+                    analyticsProto.getType(),
+                    analyticsProto.getIsAdditionalCall(),
+                    analyticsProto.getIsInterrupted(),
+                    analyticsProto.getCallTechnologies(),
+                    analyticsProto.getCallTerminationCode(),
+                    analyticsProto.getIsEmergencyCall(),
+                    analyticsProto.connectionService[0],
+                    analyticsProto.getIsCreatedFromExistingConnection(),
+                    events,
+                    timings);
+
+            result.setIsVideoCall(analyticsProto.getIsVideoCall());
+            result.setVideoEvents(Arrays.stream(analyticsProto.videoEvents)
+                    .map(videoEventProto -> new ParcelableCallAnalytics.VideoEvent(
+                            videoEventProto.getEventName(),
+                            videoEventProto.getTimeSinceLastEventMillis(),
+                            videoEventProto.getVideoState())
+                    ).collect(Collectors.toList()));
+
+            return result;
+        }
+
+        public TelecomLogClass.CallLog toProto() {
+            TelecomLogClass.CallLog result = new TelecomLogClass.CallLog();
+            result.setStartTime5Min(
+                    startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+
             // Rounds up to the nearest second.
             long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime;
             callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ?
                     0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND);
-            return new ParcelableCallAnalytics(
-                    // rounds down to nearest 5 minute mark
-                    startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES,
-                    callDuration,
-                    callDirection,
-                    isAdditionalCall,
-                    isInterrupted,
-                    callTechnologies,
-                    callTerminationReason == null ?
-                            ParcelableCallAnalytics.STILL_CONNECTED :
-                            callTerminationReason.getCode(),
-                    isEmergency,
-                    connectionService,
-                    createdFromExistingConnection);
+            result.setCallDurationMillis(callDuration);
+
+            result.setType(callDirection)
+                    .setIsAdditionalCall(isAdditionalCall)
+                    .setIsInterrupted(isInterrupted)
+                    .setCallTechnologies(callTechnologies)
+                    .setCallTerminationCode(
+                            callTerminationReason == null ?
+                                    ParcelableCallAnalytics.STILL_CONNECTED :
+                                    callTerminationReason.getCode())
+                    .setIsEmergencyCall(isEmergency)
+                    .setIsCreatedFromExistingConnection(createdFromExistingConnection)
+                    .setIsEmergencyCall(isEmergency)
+                    .setIsVideoCall(isVideo)
+                    .setConnectionProperties(callProperties);
+
+            result.connectionService = new String[] {connectionService};
+            if (callEvents != null) {
+                result.callEvents = convertLogEventsToProtoEvents(callEvents.getEvents());
+                result.callTimings = callEvents.extractEventTimings().stream()
+                        .map(Analytics::logEventTimingToProtoEventTiming)
+                        .toArray(TelecomLogClass.EventTimingEntry[]::new);
+            }
+            result.videoEvents =
+                    videoEvents.toArray(new TelecomLogClass.VideoEvent[videoEvents.size()]);
+            result.inCallServices = inCallServiceInfos.toArray(
+                    new TelecomLogClass.InCallServiceInfo[inCallServiceInfos.size()]);
+
+            return result;
         }
 
         private String getCallDirectionString() {
@@ -233,6 +479,21 @@
                 return "NOT SET";
             }
         }
+
+        private String getInCallServicesString() {
+            StringBuilder s = new StringBuilder();
+            s.append("[\n");
+            for (TelecomLogClass.InCallServiceInfo service : inCallServiceInfos) {
+                s.append("    ");
+                s.append("name: ");
+                s.append(service.getInCallServiceName());
+                s.append(" type: ");
+                s.append(service.getInCallServiceType());
+                s.append("\n");
+            }
+            s.append("]");
+            return s.toString();
+        }
     }
     public static final String TAG = "TelecomAnalytics";
 
@@ -248,10 +509,30 @@
     public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE;
     public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE;
 
+    // Constants for video events
+    public static final int SEND_LOCAL_SESSION_MODIFY_REQUEST =
+            ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_REQUEST;
+    public static final int SEND_LOCAL_SESSION_MODIFY_RESPONSE =
+            ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_RESPONSE;
+    public static final int RECEIVE_REMOTE_SESSION_MODIFY_REQUEST =
+            ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST;
+    public static final int RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE =
+            ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE;
+
     public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND;
 
     private static final Object sLock = new Object(); // Coarse lock for all of analytics
     private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>();
+    private static final List<SessionTiming> sSessionTimings = new LinkedList<>();
+
+    public static void addSessionTiming(String sessionName, long time) {
+        if (sLogSessionToSessionId.containsKey(sessionName)) {
+            synchronized (sLock) {
+                sSessionTimings.add(new SessionTiming(sLogSessionToSessionId.get(sessionName),
+                        time));
+            }
+        }
+    }
 
     public static CallInfo initiateCallAnalytics(String callId, int direction) {
         Log.d(TAG, "Starting analytics for call " + callId);
@@ -262,26 +543,77 @@
         return callInfo;
     }
 
-    public static ParcelableCallAnalytics[] dumpToParcelableAnalytics() {
-        ParcelableCallAnalytics[] result;
+    public static TelecomAnalytics dumpToParcelableAnalytics() {
+        List<ParcelableCallAnalytics> calls = new LinkedList<>();
+        List<SessionTiming> sessionTimings = new LinkedList<>();
         synchronized (sLock) {
-            result = new ParcelableCallAnalytics[sCallIdToInfo.size()];
-            int idx = 0;
-            for (CallInfoImpl entry : sCallIdToInfo.values()) {
-                result[idx] = entry.toParcelableAnalytics();
-                idx++;
-            }
+            calls.addAll(sCallIdToInfo.values().stream()
+                    .map(CallInfoImpl::toParcelableAnalytics)
+                    .collect(Collectors.toList()));
+            sessionTimings.addAll(sSessionTimings);
             sCallIdToInfo.clear();
+            sSessionTimings.clear();
         }
-        return result;
+        return new TelecomAnalytics(sessionTimings, calls);
+    }
+
+    public static void dumpToEncodedProto(PrintWriter pw, String[] args) {
+        TelecomLogClass.TelecomLog result = new TelecomLogClass.TelecomLog();
+
+        synchronized (sLock) {
+            result.callLogs = sCallIdToInfo.values().stream()
+                    .map(CallInfoImpl::toProto)
+                    .toArray(TelecomLogClass.CallLog[]::new);
+            result.sessionTimings = sSessionTimings.stream()
+                    .map(timing -> new TelecomLogClass.LogSessionTiming()
+                            .setSessionEntryPoint(timing.getKey())
+                            .setTimeMillis(timing.getTime()))
+                    .toArray(TelecomLogClass.LogSessionTiming[]::new);
+            if (args.length > 1 && CLEAR_ANALYTICS_ARG.equals(args[1])) {
+                sCallIdToInfo.clear();
+                sSessionTimings.clear();
+            }
+        }
+        String encodedProto = Base64.encodeToString(
+                TelecomLogClass.TelecomLog.toByteArray(result), Base64.DEFAULT);
+        pw.write(encodedProto);
     }
 
     public static void dump(IndentingPrintWriter writer) {
         synchronized (sLock) {
-            for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
-                writer.printf("Call %s: ", entry.getKey());
-                writer.println(entry.getValue().toString());
+            int prefixLength = CallsManager.TELECOM_CALL_ID_PREFIX.length();
+            List<String> callIds = new ArrayList<>(sCallIdToInfo.keySet());
+            // Sort the analytics in increasing order of call IDs
+            try {
+                Collections.sort(callIds, (id1, id2) -> {
+                    int i1, i2;
+                    try {
+                        i1 = Integer.valueOf(id1.substring(prefixLength));
+                    } catch (NumberFormatException e) {
+                        i1 = Integer.MAX_VALUE;
+                    }
+
+                    try {
+                        i2 = Integer.valueOf(id2.substring(prefixLength));
+                    } catch (NumberFormatException e) {
+                        i2 = Integer.MAX_VALUE;
+                    }
+                    return i1 - i2;
+                });
+            } catch (IllegalArgumentException e) {
+                // do nothing, leave the list in a partially sorted state.
             }
+
+            for (String callId : callIds) {
+                writer.printf("Call %s: ", callId);
+                writer.println(sCallIdToInfo.get(callId).toString());
+            }
+
+            Map<Integer, Double> averageTimings = SessionTiming.averageTimings(sSessionTimings);
+            averageTimings.entrySet().stream()
+                    .filter(e -> sSessionIdToLogSession.containsKey(e.getKey()))
+                    .forEach(e -> writer.printf("%s: %.2f\n",
+                            sSessionIdToLogSession.get(e.getKey()), e.getValue()));
         }
     }
 
@@ -292,8 +624,9 @@
     }
 
     /**
-     * Returns a deep copy of callIdToInfo that's safe to read/write without synchronization
+     * Returns a copy of callIdToInfo. Use only for testing.
      */
+    @VisibleForTesting
     public static Map<String, CallInfoImpl> cloneData() {
         synchronized (sLock) {
             Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size());
@@ -303,4 +636,43 @@
             return result;
         }
     }
+
+    private static TelecomLogClass.Event[] convertLogEventsToProtoEvents(
+            List<Log.CallEvent> logEvents) {
+        long timeOfLastEvent = -1;
+        ArrayList<TelecomLogClass.Event> events = new ArrayList<>(logEvents.size());
+        for (Log.CallEvent logEvent : logEvents) {
+            if (sLogEventToAnalyticsEvent.containsKey(logEvent.eventId)) {
+                TelecomLogClass.Event event = new TelecomLogClass.Event();
+                event.setEventName(sLogEventToAnalyticsEvent.get(logEvent.eventId));
+                event.setTimeSinceLastEventMillis(roundToOneSigFig(
+                        timeOfLastEvent < 0 ? -1 : logEvent.time - timeOfLastEvent));
+                events.add(event);
+                timeOfLastEvent = logEvent.time;
+            }
+        }
+        return events.toArray(new TelecomLogClass.Event[events.size()]);
+    }
+
+    private static TelecomLogClass.EventTimingEntry logEventTimingToProtoEventTiming(
+            Log.CallEventRecord.EventTiming logEventTiming) {
+        int analyticsEventTimingName =
+                sLogEventTimingToAnalyticsEventTiming.containsKey(logEventTiming.name) ?
+                        sLogEventTimingToAnalyticsEventTiming.get(logEventTiming.name) :
+                        ParcelableCallAnalytics.EventTiming.INVALID;
+        return new TelecomLogClass.EventTimingEntry()
+                .setTimingName(analyticsEventTimingName)
+                .setTimeMillis(logEventTiming.time);
+    }
+
+    @VisibleForTesting
+    public static long roundToOneSigFig(long val)  {
+        if (val == 0) {
+            return val;
+        }
+        int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val));
+        double s = Math.pow(10, logVal);
+        double dec =  val / s;
+        return (long) (Math.round(dec) * s);
+    }
 }
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
index 3819879..d31c69d 100644
--- a/src/com/android/server/telecom/BluetoothManager.java
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -121,7 +121,7 @@
 
     private BluetoothHeadsetProxy mBluetoothHeadset;
     private long mBluetoothConnectionRequestTime;
-    private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA") {
+    private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA", null /*lock*/) {
         @Override
         public void loggedRun() {
             if (!isBluetoothAudioConnected()) {
@@ -132,7 +132,7 @@
         }
     };
 
-    private final Runnable mRetryConnectAudio = new Runnable("BM.rCA") {
+    private final Runnable mRetryConnectAudio = new Runnable("BM.rCA", null /*lock*/) {
         @Override
         public void loggedRun() {
             Log.i(this, "Retrying connecting to bluetooth audio.");
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index a72a21b..c79b036 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -30,6 +30,7 @@
 import android.os.RemoteException;
 import android.telecom.Connection;
 import android.telecom.PhoneAccount;
+import android.telecom.VideoProfile;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -98,7 +99,7 @@
                     Log.i(TAG, "BT - answering call");
                     Call call = mCallsManager.getRingingCall();
                     if (call != null) {
-                        mCallsManager.answerCall(call, call.getVideoState());
+                        mCallsManager.answerCall(call, VideoProfile.STATE_AUDIO_ONLY);
                         return true;
                     }
                     return false;
@@ -291,11 +292,17 @@
     public CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
         @Override
         public void onCallAdded(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             updateHeadsetWithCallState(false /* force */);
         }
 
         @Override
         public void onCallRemoved(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             mClccIndexMap.remove(call);
             updateHeadsetWithCallState(false /* force */);
         }
@@ -318,6 +325,9 @@
 
         @Override
         public void onCallStateChanged(Call call, int oldState, int newState) {
+            if (call.isExternalCall()) {
+                return;
+            }
             // If a call is being put on hold because of a new connecting call, ignore the
             // CONNECTING since the BT state update needs to send out the numHeld = 1 + dialing
             // state atomically.
@@ -335,7 +345,8 @@
             // state. We can assume that the active call will be automatically held which will
             // send another update at which point we will be in the right state.
             if (mCallsManager.getActiveCall() != null
-                    && oldState == CallState.CONNECTING && newState == CallState.DIALING) {
+                    && oldState == CallState.CONNECTING &&
+                    (newState == CallState.DIALING || newState == CallState.PULLING)) {
                 return;
             }
             updateHeadsetWithCallState(false /* force */);
@@ -343,6 +354,9 @@
 
         @Override
         public void onIsConferencedChanged(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             /*
              * Filter certain onIsConferencedChanged callbacks. Unfortunately this needs to be done
              * because conference change events are not atomic and multiple callbacks get fired
@@ -487,6 +501,12 @@
                 return false;
             if (activeCall != null) {
                 mCallsManager.disconnectCall(activeCall);
+                if (ringingCall != null) {
+                    mCallsManager.answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
+                } else if (heldCall != null) {
+                    mCallsManager.unholdCall(heldCall);
+                }
+                return true;
             }
             if (ringingCall != null) {
                 mCallsManager.answerCall(ringingCall, ringingCall.getVideoState());
@@ -501,7 +521,7 @@
                 updateHeadsetWithCallState(true /* force */);
                 return true;
             } else if (ringingCall != null) {
-                mCallsManager.answerCall(ringingCall, ringingCall.getVideoState());
+                mCallsManager.answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
                 return true;
             } else if (heldCall != null) {
                 // CallsManager will hold any active calls when unhold() is called on a
@@ -811,6 +831,7 @@
             case CallState.CONNECTING:
             case CallState.SELECT_PHONE_ACCOUNT:
             case CallState.DIALING:
+            case CallState.PULLING:
                 // Yes, this is correctly returning ALERTING.
                 // "Dialing" for BT means that we have sent information to the service provider
                 // to place the call but there is no confirmation that the call is going through.
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 4c70913..98310bd 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -103,7 +103,7 @@
         void onExtrasRemoved(Call c, int source, List<String> keys);
         void onHandleChanged(Call call);
         void onCallerDisplayNameChanged(Call call);
-        void onVideoStateChanged(Call call);
+        void onVideoStateChanged(Call call, int previousVideoState, int newVideoState);
         void onTargetPhoneAccountChanged(Call call);
         void onConnectionManagerPhoneAccountChanged(Call call);
         void onPhoneAccountChanged(Call call);
@@ -160,7 +160,7 @@
         @Override
         public void onCallerDisplayNameChanged(Call call) {}
         @Override
-        public void onVideoStateChanged(Call call) {}
+        public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {}
         @Override
         public void onTargetPhoneAccountChanged(Call call) {}
         @Override
@@ -343,6 +343,7 @@
     private final CallsManager mCallsManager;
     private final TelecomSystem.SyncRoot mLock;
     private final String mId;
+    private String mConnectionId;
     private Analytics.CallInfo mAnalytics;
 
     private boolean mWasConferencePreviouslyMerged = false;
@@ -379,6 +380,8 @@
      */
     private boolean mIsVideoCallingSupported = false;
 
+    private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+
     /**
      * Persists the specified parameters and initializes the new instance.
      *
@@ -404,6 +407,7 @@
             ConnectionServiceRepository repository,
             ContactsAsyncHelper contactsAsyncHelper,
             CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
+            PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
             Uri handle,
             GatewayInfo gatewayInfo,
             PhoneAccountHandle connectionManagerPhoneAccountHandle,
@@ -412,11 +416,13 @@
             boolean shouldAttachToExistingConnection,
             boolean isConference) {
         mId = callId;
+        mConnectionId = callId;
         mState = isConference ? CallState.ACTIVE : CallState.NEW;
         mContext = context;
         mCallsManager = callsManager;
         mLock = lock;
         mRepository = repository;
+        mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
         setHandle(handle);
         mPostDialDigits = handle != null
                 ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
@@ -430,7 +436,6 @@
         maybeLoadCannedSmsResponses();
         mAnalytics = new Analytics.CallInfo();
 
-        Log.event(this, Log.Events.CREATED);
     }
 
     /**
@@ -459,6 +464,7 @@
             ConnectionServiceRepository repository,
             ContactsAsyncHelper contactsAsyncHelper,
             CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
+            PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
             Uri handle,
             GatewayInfo gatewayInfo,
             PhoneAccountHandle connectionManagerPhoneAccountHandle,
@@ -468,7 +474,7 @@
             boolean isConference,
             long connectTimeMillis) {
         this(callId, context, callsManager, lock, repository, contactsAsyncHelper,
-                callerInfoAsyncQueryFactory, handle, gatewayInfo,
+                callerInfoAsyncQueryFactory, phoneNumberUtilsAdapter, handle, gatewayInfo,
                 connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection,
                 shouldAttachToExistingConnection, isConference);
 
@@ -501,6 +507,7 @@
                 analyticsDirection = Analytics.UNKNOWN_DIRECTION;
         }
         mAnalytics = Analytics.initiateCallAnalytics(mId, analyticsDirection);
+        Log.event(this, Log.Events.CREATED);
     }
 
     public Analytics.CallInfo getAnalytics() {
@@ -570,6 +577,11 @@
             return false;
         }
 
+        // Only Redial a Call in the case of it being an Emergency Call.
+        if(!isEmergencyCall()) {
+            return false;
+        }
+
         // Make sure that there are additional connection services to process.
         if (mCreateConnectionProcessor == null
             || !mCreateConnectionProcessor.isProcessingComplete()
@@ -595,6 +607,21 @@
     }
 
     /**
+     * Returns the unique ID for this call (see {@link #getId}) along with an attempt indicator that
+     * iterates based on attempts to establish a {@link Connection} using createConnectionProcessor.
+     * @return The call ID with an appended attempt id.
+     */
+    public String getConnectionId() {
+        if(mCreateConnectionProcessor != null) {
+            mConnectionId = mId + "_" +
+                    String.valueOf(mCreateConnectionProcessor.getConnectionAttempt());
+            return mConnectionId;
+        } else {
+            return mConnectionId;
+        }
+    }
+
+    /**
      * Sets the call state. Although there exists the notion of appropriate state transitions
      * (see {@link CallState}), in practice those expectations break down when cellular systems
      * misbehave and they do this very often. The result is that we do not enforce state transitions
@@ -655,6 +682,9 @@
                 case CallState.DIALING:
                     event = Log.Events.SET_DIALING;
                     break;
+                case CallState.PULLING:
+                    event = Log.Events.SET_PULLING;
+                    break;
                 case CallState.DISCONNECTED:
                     event = Log.Events.SET_DISCONNECTED;
                     data = getDisconnectCause();
@@ -747,8 +777,9 @@
             // Let's not allow resetting of the emergency flag. Once a call becomes an emergency
             // call, it will remain so for the rest of it's lifetime.
             if (!mIsEmergencyCall) {
-                mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
-                        mContext, mHandle.getSchemeSpecificPart());
+                mIsEmergencyCall = mHandle != null &&
+                        mPhoneNumberUtilsAdapter.isLocalEmergencyNumber(mContext,
+                                mHandle.getSchemeSpecificPart());
             }
             startCallerInfoLookup();
             for (Listener l : mListeners) {
@@ -982,10 +1013,18 @@
                 connectionCapabilities = removeVideoCapabilities(connectionCapabilities);
             }
 
+            int previousCapabilities = mConnectionCapabilities;
             mConnectionCapabilities = connectionCapabilities;
             for (Listener l : mListeners) {
                 l.onConnectionCapabilitiesChanged(this);
             }
+
+            int xorCaps = previousCapabilities ^ mConnectionCapabilities;
+            Log.event(this, Log.Events.CAPABILITY_CHANGE,
+                    "Current: [%s], Removed [%s], Added [%s]",
+                    Connection.capabilitiesToStringShort(mConnectionCapabilities),
+                    Connection.capabilitiesToStringShort(previousCapabilities & xorCaps),
+                    Connection.capabilitiesToStringShort(mConnectionCapabilities & xorCaps));
         }
     }
 
@@ -1006,12 +1045,21 @@
             if (wasExternal != isExternal) {
                 Log.v(this, "setConnectionProperties: external call changed isExternal = %b",
                         isExternal);
-
+                Log.event(this, Log.Events.IS_EXTERNAL, isExternal);
                 for (Listener l : mListeners) {
                     l.onExternalCallChanged(this, isExternal);
                 }
 
             }
+
+            mAnalytics.addCallProperties(mConnectionProperties);
+
+            int xorProps = previousProperties ^ mConnectionProperties;
+            Log.event(this, Log.Events.PROPERTY_CHANGE,
+                    "Current: [%s], Removed [%s], Added [%s]",
+                    Connection.propertiesToStringShort(mConnectionProperties),
+                    Connection.propertiesToStringShort(previousProperties & xorProps),
+                    Connection.propertiesToStringShort(mConnectionProperties & xorProps));
         }
     }
 
@@ -1282,16 +1330,24 @@
      */
     @VisibleForTesting
     public void answer(int videoState) {
-        Preconditions.checkNotNull(mConnectionService);
-
         // Check to verify that the call is still in the ringing state. A call can change states
         // between the time the user hits 'answer' and Telecom receives the command.
         if (isRinging("answer")) {
+            if (!isVideoCallingSupported() && VideoProfile.isVideo(videoState)) {
+                // Video calling is not supported, yet the InCallService is attempting to answer as
+                // video.  We will simply answer as audio-only.
+                videoState = VideoProfile.STATE_AUDIO_ONLY;
+            }
             // At this point, we are asking the connection service to answer but we don't assume
             // that it will work. Instead, we wait until confirmation from the connectino service
             // that the call is in a non-STATE_RINGING state before changing the UI. See
             // {@link ConnectionServiceAdapter#setActive} and other set* methods.
-            mConnectionService.answer(this, videoState);
+            if (mConnectionService != null) {
+                mConnectionService.answer(this, videoState);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "answer call failed due to null CS callId=%s", getId());
+            }
             Log.event(this, Log.Events.REQUEST_ACCEPT);
         }
     }
@@ -1304,16 +1360,20 @@
      */
     @VisibleForTesting
     public void reject(boolean rejectWithMessage, String textMessage) {
-        Preconditions.checkNotNull(mConnectionService);
-
         // Check to verify that the call is still in the ringing state. A call can change states
         // between the time the user hits 'reject' and Telecomm receives the command.
         if (isRinging("reject")) {
             // Ensure video state history tracks video state at time of rejection.
             mVideoStateHistory |= mVideoState;
 
-            mConnectionService.reject(this, rejectWithMessage, textMessage);
+            if (mConnectionService != null) {
+                mConnectionService.reject(this, rejectWithMessage, textMessage);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "reject call failed due to null CS callId=%s", getId());
+            }
             Log.event(this, Log.Events.REQUEST_REJECT);
+
         }
     }
 
@@ -1321,10 +1381,13 @@
      * Puts the call on hold if it is currently active.
      */
     void hold() {
-        Preconditions.checkNotNull(mConnectionService);
-
         if (mState == CallState.ACTIVE) {
-            mConnectionService.hold(this);
+            if (mConnectionService != null) {
+                mConnectionService.hold(this);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "hold call failed due to null CS callId=%s", getId());
+            }
             Log.event(this, Log.Events.REQUEST_HOLD);
         }
     }
@@ -1333,10 +1396,13 @@
      * Releases the call from hold if it is currently active.
      */
     void unhold() {
-        Preconditions.checkNotNull(mConnectionService);
-
         if (mState == CallState.ON_HOLD) {
-            mConnectionService.unhold(this);
+            if (mConnectionService != null) {
+                mConnectionService.unhold(this);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "unhold call failed due to null CS callId=%s", getId());
+            }
             Log.event(this, Log.Events.REQUEST_UNHOLD);
         }
     }
@@ -1390,7 +1456,12 @@
 
         // If the change originated from an InCallService, notify the connection service.
         if (source == SOURCE_INCALL_SERVICE) {
-            mConnectionService.onExtrasChanged(this, mExtras);
+            if (mConnectionService != null) {
+                mConnectionService.onExtrasChanged(this, mExtras);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "putExtras failed due to null CS callId=%s", getId());
+            }
         }
     }
 
@@ -1420,7 +1491,12 @@
 
         // If the change originated from an InCallService, notify the connection service.
         if (source == SOURCE_INCALL_SERVICE) {
-            mConnectionService.onExtrasChanged(this, mExtras);
+            if (mConnectionService != null) {
+                mConnectionService.onExtrasChanged(this, mExtras);
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "removeExtras failed due to null CS callId=%s", getId());
+            }
         }
     }
 
@@ -1461,7 +1537,12 @@
     }
 
     void postDialContinue(boolean proceed) {
-        mConnectionService.onPostDialContinue(this, proceed);
+        if (mConnectionService != null) {
+            mConnectionService.onPostDialContinue(this, proceed);
+        } else {
+            Log.e(this, new NullPointerException(),
+                    "postDialContinue failed due to null CS callId=%s", getId());
+        }
     }
 
     void conferenceWith(Call otherCall) {
@@ -1477,7 +1558,7 @@
         if (mConnectionService == null) {
             Log.w(this, "splitting from conference call without a connection service");
         } else {
-            Log.event(this, Log.Events.SPLIT_CONFERENCE);
+            Log.event(this, Log.Events.SPLIT_FROM_CONFERENCE);
             mConnectionService.splitFromConference(this);
         }
     }
@@ -1525,7 +1606,7 @@
      * {@link android.telecom.Connection#PROPERTY_IS_EXTERNAL_CALL} property set.
      * <p>
      * An external call is a representation of a call which is taking place on another device
-     * associated with a PhoneAccount on this device.  Issuing a request to pull the external call 
+     * associated with a PhoneAccount on this device.  Issuing a request to pull the external call
      * tells the {@link android.telecom.ConnectionService} that it should move the call from the
      * other device to this one.  An example of this is the IMS multi-endpoint functionality.  A
      * user may have two phones with the same phone number.  If the user is engaged in an active
@@ -1550,7 +1631,7 @@
             return;
         }
 
-        Log.event(this, Log.Events.PULL);
+        Log.event(this, Log.Events.REQUEST_PULL);
         mConnectionService.pullExternalCall(this);
     }
 
@@ -1563,7 +1644,12 @@
      * @param extras Associated extras.
      */
     public void sendCallEvent(String event, Bundle extras) {
-        mConnectionService.sendCallEvent(this, event, extras);
+        if (mConnectionService != null) {
+            mConnectionService.sendCallEvent(this, event, extras);
+        } else {
+            Log.e(this, new NullPointerException(),
+                    "sendCallEvent failed due to null CS callId=%s", getId());
+        }
     }
 
     void setParentCall(Call parentCall) {
@@ -1671,7 +1757,7 @@
             return false;
         }
 
-        if (PhoneNumberUtils.isUriNumber(getHandle().toString())) {
+        if (mPhoneNumberUtilsAdapter.isUriNumber(getHandle().toString())) {
             // The incoming number is actually a URI (i.e. a SIP address),
             // not a regular PSTN phone number, and we can't send SMSes to
             // SIP addresses.
@@ -1745,7 +1831,10 @@
      */
     private void setCallerInfo(Uri handle, CallerInfo callerInfo) {
         Trace.beginSection("setCallerInfo");
-        Preconditions.checkNotNull(callerInfo);
+        if (callerInfo == null) {
+            Log.i(this, "CallerInfo lookup returned null, skipping update");
+            return;
+        }
 
         if (!handle.equals(mHandle)) {
             Log.i(this, "setCallerInfo received stale caller info for an old handle. Ignoring.");
@@ -1884,6 +1973,12 @@
      * @param videoState The video state for the call.
      */
     public void setVideoState(int videoState) {
+        // If the phone account associated with this call does not support video calling, then we
+        // will automatically set the video state to audio-only.
+        if (!isVideoCallingSupported()) {
+            videoState = VideoProfile.STATE_AUDIO_ONLY;
+        }
+
         // Track which video states were applicable over the duration of the call.
         // Only track the call state when the call is active or disconnected.  This ensures we do
         // not include the video state when:
@@ -1895,9 +1990,18 @@
             mVideoStateHistory = mVideoStateHistory | videoState;
         }
 
+        int previousVideoState = mVideoState;
         mVideoState = videoState;
-        for (Listener l : mListeners) {
-            l.onVideoStateChanged(this);
+        if (mVideoState != previousVideoState) {
+            Log.event(this, Log.Events.VIDEO_STATE_CHANGED,
+                    VideoProfile.videoStateToString(videoState));
+            for (Listener l : mListeners) {
+                l.onVideoStateChanged(this, previousVideoState, mVideoState);
+            }
+        }
+
+        if (VideoProfile.isVideo(videoState)) {
+            mAnalytics.setCallIsVideo(true);
         }
     }
 
@@ -1969,6 +2073,8 @@
                 return CallState.ACTIVE;
             case Connection.STATE_DIALING:
                 return CallState.DIALING;
+            case Connection.STATE_PULLING_CALL:
+                return CallState.PULLING;
             case Connection.STATE_DISCONNECTED:
                 return CallState.DISCONNECTED;
             case Connection.STATE_HOLDING:
@@ -2045,6 +2151,7 @@
      * @param extras The extras.
      */
     public void onConnectionEvent(String event, Bundle extras) {
+        Log.event(this, Log.Events.CONNECTION_EVENT, event);
         if (Connection.EVENT_ON_HOLD_TONE_START.equals(event)) {
             mIsRemotelyHeld = true;
             Log.event(this, Log.Events.REMOTELY_HELD);
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 7bc727c..236df5f 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -72,6 +72,7 @@
             put(CallState.CONNECTING, mActiveDialingOrConnectingCalls);
             put(CallState.ACTIVE, mActiveDialingOrConnectingCalls);
             put(CallState.DIALING, mActiveDialingOrConnectingCalls);
+            put(CallState.PULLING, mActiveDialingOrConnectingCalls);
             put(CallState.RINGING, mRingingCalls);
             put(CallState.ON_HOLD, mHoldingCalls);
         }};
@@ -105,7 +106,7 @@
         }
 
         updateForegroundCall();
-        if (newState == CallState.DISCONNECTED) {
+        if (shouldPlayDisconnectTone(oldState, newState)) {
             playToneForDisconnectedCall(call);
         }
 
@@ -177,12 +178,20 @@
      */
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
-       if (isExternalCall) {
+        if (isExternalCall) {
             Log.d(LOG_TAG, "Removing call which became external ID %s", call.getId());
             removeCall(call);
         } else if (!isExternalCall) {
             Log.d(LOG_TAG, "Adding external call which was pulled with ID %s", call.getId());
             addCall(call);
+
+            if (mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(call.getVideoState())) {
+                // When pulling a video call, automatically enable the speakerphone.
+                Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." +
+                        call.getId());
+                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                        CallAudioRouteStateMachine.SWITCH_SPEAKER);
+            }
         }
     }
 
@@ -314,6 +323,25 @@
                 CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
     }
 
+    @Override
+    public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
+        if (call != getForegroundCall()) {
+            Log.d(LOG_TAG, "Ignoring video state change from %s to %s for call %s -- not " +
+                    "foreground.", VideoProfile.videoStateToString(previousVideoState),
+                    VideoProfile.videoStateToString(newVideoState), call.getId());
+            return;
+        }
+
+        if (!VideoProfile.isVideo(previousVideoState) &&
+                mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(newVideoState)) {
+            Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" +
+                    " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState),
+                    VideoProfile.videoStateToString(newVideoState));
+            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.SWITCH_SPEAKER);
+        }
+    }
+
     public CallAudioState getCallAudioState() {
         return mCallAudioRouteStateMachine.getCurrentCallAudioState();
     }
@@ -394,8 +422,8 @@
     }
 
     @VisibleForTesting
-    public void startRinging() {
-        mRinger.startRinging(mForegroundCall);
+    public boolean startRinging() {
+        return mRinger.startRinging(mForegroundCall);
     }
 
     @VisibleForTesting
@@ -424,6 +452,11 @@
         return mCallAudioRouteStateMachine;
     }
 
+    @VisibleForTesting
+    public CallAudioModeStateMachine getCallAudioModeStateMachine() {
+        return mCallAudioModeStateMachine;
+    }
+
     void dump(IndentingPrintWriter pw) {
         pw.println("All calls:");
         pw.increaseIndent();
@@ -470,9 +503,13 @@
             case CallState.ON_HOLD:
                 onCallLeavingHold();
                 break;
+            case CallState.PULLING:
+                onCallLeavingActiveDialingOrConnecting();
+                break;
             case CallState.DIALING:
                 stopRingbackForCall(call);
                 onCallLeavingActiveDialingOrConnecting();
+                break;
         }
     }
 
@@ -488,6 +525,9 @@
             case CallState.ON_HOLD:
                 onCallEnteringHold();
                 break;
+            case CallState.PULLING:
+                onCallEnteringActiveDialingOrConnecting();
+                break;
             case CallState.DIALING:
                 onCallEnteringActiveDialingOrConnecting();
                 playRingbackForCall(call);
@@ -643,7 +683,7 @@
         if (shouldPlayHoldTone()) {
             if (mHoldTonePlayer == null) {
                 mHoldTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
-                mHoldTonePlayer.start();
+                mHoldTonePlayer.startTone();
             }
         } else {
             if (mHoldTonePlayer != null) {
@@ -697,6 +737,15 @@
         }
     }
 
+    private boolean shouldPlayDisconnectTone(int oldState, int newState) {
+        if (newState != CallState.DISCONNECTED) {
+            return false;
+        }
+        return oldState == CallState.ACTIVE ||
+                oldState == CallState.DIALING ||
+                oldState == CallState.ON_HOLD;
+    }
+
     @VisibleForTesting
     public Set<Call> getTrackedCalls() {
         return mCalls;
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 92adca6..57043bc 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -201,19 +201,22 @@
         @Override
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering RINGING state");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            if (mMostRecentMode == AudioManager.MODE_IN_CALL) {
-                // Preserving behavior from the old CallAudioManager.
-                Log.i(LOG_TAG, "Transition from IN_CALL -> RINGTONE."
-                        + "  Resetting to NORMAL first.");
-                mAudioManager.setMode(AudioManager.MODE_NORMAL);
+            if (mCallAudioManager.startRinging()) {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                if (mMostRecentMode == AudioManager.MODE_IN_CALL) {
+                    // Preserving behavior from the old CallAudioManager.
+                    Log.i(LOG_TAG, "Transition from IN_CALL -> RINGTONE."
+                            + "  Resetting to NORMAL first.");
+                    mAudioManager.setMode(AudioManager.MODE_NORMAL);
+                }
+                mAudioManager.setMode(AudioManager.MODE_RINGTONE);
+                mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.RINGING_FOCUS);
+            } else {
+                Log.i(LOG_TAG, "Entering RINGING but not acquiring focus -- silent ringtone");
             }
-            mAudioManager.setMode(AudioManager.MODE_RINGTONE);
 
             mCallAudioManager.stopCallWaiting();
-            mCallAudioManager.startRinging();
-            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
         }
 
         @Override
@@ -288,7 +291,7 @@
                     AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             mAudioManager.setMode(AudioManager.MODE_IN_CALL);
             mMostRecentMode = AudioManager.MODE_IN_CALL;
-            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
         }
 
         @Override
@@ -350,7 +353,7 @@
                     AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
             mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
-            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
         }
 
         @Override
@@ -407,7 +410,7 @@
             mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
                     AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             mAudioManager.setMode(mMostRecentMode);
-            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
         }
 
         @Override
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 33700f7..ce03e61 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -18,7 +18,11 @@
 
 
 import android.app.ActivityManagerNative;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.UserInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
@@ -105,7 +109,15 @@
 
     /** Valid values for mAudioFocusType */
     public static final int NO_FOCUS = 1;
-    public static final int HAS_FOCUS = 2;
+    public static final int ACTIVE_FOCUS = 2;
+    public static final int RINGING_FOCUS = 3;
+
+    private static final SparseArray<String> AUDIO_ROUTE_TO_LOG_EVENT = new SparseArray<String>() {{
+        put(CallAudioState.ROUTE_BLUETOOTH, Log.Events.AUDIO_ROUTE_BT);
+        put(CallAudioState.ROUTE_EARPIECE, Log.Events.AUDIO_ROUTE_EARPIECE);
+        put(CallAudioState.ROUTE_SPEAKER, Log.Events.AUDIO_ROUTE_SPEAKER);
+        put(CallAudioState.ROUTE_WIRED_HEADSET, Log.Events.AUDIO_ROUTE_HEADSET);
+    }};
 
     private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
         put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
@@ -128,6 +140,8 @@
         put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
         put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
 
+        put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
+
         put(MUTE_ON, "MUTE_ON");
         put(MUTE_OFF, "MUTE_OFF");
         put(TOGGLE_MUTE, "TOGGLE_MUTE");
@@ -137,10 +151,39 @@
         put(RUN_RUNNABLE, "RUN_RUNNABLE");
     }};
 
+    /**
+     * BroadcastReceiver used to track changes in the notification interruption filter.  This
+     * ensures changes to the notification interruption filter made by the user during a call are
+     * respected when restoring the notification interruption filter state.
+     */
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("CARSM.oR");
+            try {
+                String action = intent.getAction();
+
+                if (action.equals(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)) {
+                    if (mAreNotificationSuppressed) {
+                        // If we've already set the interruption filter, and the user changes it to
+                        // something other than INTERRUPTION_FILTER_ALARMS, assume we will no longer
+                        // try to change it back if the audio route changes.
+                        mAreNotificationSuppressed =
+                                mInterruptionFilterProxy.getCurrentInterruptionFilter()
+                                        == NotificationManager.INTERRUPTION_FILTER_ALARMS;
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+
     private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
     private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
     private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
     private static final String ACTIVE_HEADSET_ROUTE_NAME = "ActiveHeadsetRoute";
+    private static final String RINGING_BLUETOOTH_ROUTE_NAME = "RingingBluetoothRoute";
     private static final String QUIESCENT_EARPIECE_ROUTE_NAME = "QuiescentEarpieceRoute";
     private static final String QUIESCENT_BLUETOOTH_ROUTE_NAME = "QuiescentBluetoothRoute";
     private static final String QUIESCENT_SPEAKER_ROUTE_NAME = "QuiescentSpeakerRoute";
@@ -153,7 +196,7 @@
         if (msg.obj != null && msg.obj instanceof Session) {
             String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown");
             Log.continueSession((Session) msg.obj, "CARSM.pM_" + messageCodeName);
-            Log.i(this, "Message received: %s=%d", messageCodeName, msg.what);
+            Log.i(this, "Message received: %s=%d, arg1=%d", messageCodeName, msg.what, msg.arg1);
         }
     }
 
@@ -210,6 +253,9 @@
                 case USER_SWITCH_BASELINE_ROUTE:
                     sendInternalMessage(calculateBaselineRouteMessage(true));
                     return HANDLED;
+                case SWITCH_FOCUS:
+                    mAudioFocusType = msg.arg1;
+                    return NOT_HANDLED;
                 default:
                     return NOT_HANDLED;
             }
@@ -236,6 +282,9 @@
             super.enter();
             setSpeakerphoneOn(false);
             setBluetoothOn(false);
+            if (mAudioFocusType == ACTIVE_FOCUS) {
+                setNotificationsSuppressed(true);
+            }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
                     mAvailableRoutes);
             setSystemAudioState(newState);
@@ -243,6 +292,12 @@
         }
 
         @Override
+        public void exit() {
+            super.exit();
+            setNotificationsSuppressed(false);
+        }
+
+        @Override
         public void updateSystemAudioState() {
             updateInternalCallAudioState();
             setSystemAudioState(mCurrentCallAudioState);
@@ -261,7 +316,8 @@
                 case SWITCH_BLUETOOTH:
                 case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
-                        transitionTo(mActiveBluetoothRoute);
+                        transitionTo(mAudioFocusType == ACTIVE_FOCUS ?
+                                mActiveBluetoothRoute : mRingingBluetoothRoute);
                     } else {
                         Log.w(this, "Ignoring switch to bluetooth command. Not available.");
                     }
@@ -279,6 +335,10 @@
                     transitionTo(mActiveSpeakerRoute);
                     return HANDLED;
                 case SWITCH_FOCUS:
+                    if (msg.arg1 == ACTIVE_FOCUS) {
+                        setNotificationsSuppressed(true);
+                    }
+
                     if (msg.arg1 == NO_FOCUS) {
                         reinitialize();
                     }
@@ -319,9 +379,11 @@
             }
             switch (msg.what) {
                 case SWITCH_EARPIECE:
+                case USER_SWITCH_EARPIECE:
                     // Nothing to do here
                     return HANDLED;
                 case SWITCH_BLUETOOTH:
+                case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
                         transitionTo(mQuiescentBluetoothRoute);
                     } else {
@@ -329,6 +391,7 @@
                     }
                     return HANDLED;
                 case SWITCH_HEADSET:
+                case USER_SWITCH_HEADSET:
                     if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
                         transitionTo(mQuiescentHeadsetRoute);
                     } else {
@@ -336,10 +399,11 @@
                     }
                     return HANDLED;
                 case SWITCH_SPEAKER:
+                case USER_SWITCH_SPEAKER:
                     transitionTo(mQuiescentSpeakerRoute);
                     return HANDLED;
                 case SWITCH_FOCUS:
-                    if (msg.arg1 == HAS_FOCUS) {
+                    if (msg.arg1 == ACTIVE_FOCUS || msg.arg1 == RINGING_FOCUS) {
                         transitionTo(mActiveEarpieceRoute);
                     }
                     return HANDLED;
@@ -438,7 +502,8 @@
                 case SWITCH_BLUETOOTH:
                 case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
-                        transitionTo(mActiveBluetoothRoute);
+                        transitionTo(mAudioFocusType == ACTIVE_FOCUS ?
+                                mActiveBluetoothRoute : mRingingBluetoothRoute);
                     } else {
                         Log.w(this, "Ignoring switch to bluetooth command. Not available.");
                     }
@@ -492,6 +557,7 @@
             }
             switch (msg.what) {
                 case SWITCH_EARPIECE:
+                case USER_SWITCH_EARPIECE:
                     if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
                         transitionTo(mQuiescentEarpieceRoute);
                     } else {
@@ -499,6 +565,7 @@
                     }
                     return HANDLED;
                 case SWITCH_BLUETOOTH:
+                case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
                         transitionTo(mQuiescentBluetoothRoute);
                     } else {
@@ -506,13 +573,15 @@
                     }
                     return HANDLED;
                 case SWITCH_HEADSET:
+                case USER_SWITCH_HEADSET:
                     // Nothing to do
                     return HANDLED;
                 case SWITCH_SPEAKER:
+                case USER_SWITCH_SPEAKER:
                     transitionTo(mQuiescentSpeakerRoute);
                     return HANDLED;
                 case SWITCH_FOCUS:
-                    if (msg.arg1 == HAS_FOCUS) {
+                    if (msg.arg1 == ACTIVE_FOCUS || msg.arg1 == RINGING_FOCUS) {
                         transitionTo(mActiveHeadsetRoute);
                     }
                     return HANDLED;
@@ -637,6 +706,8 @@
                 case SWITCH_FOCUS:
                     if (msg.arg1 == NO_FOCUS) {
                         reinitialize();
+                    } else if (msg.arg1 == RINGING_FOCUS) {
+                        transitionTo(mRingingBluetoothRoute);
                     }
                     return HANDLED;
                 case BT_AUDIO_DISCONNECT:
@@ -648,6 +719,87 @@
         }
     }
 
+    class RingingBluetoothRoute extends BluetoothRoute {
+        @Override
+        public String getName() {
+            return RINGING_BLUETOOTH_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            setSpeakerphoneOn(false);
+            // Do not enable SCO audio here, since RING is being sent to the headset.
+            CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
+                    mAvailableRoutes);
+            setSystemAudioState(newState);
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            updateInternalCallAudioState();
+            setSystemAudioState(mCurrentCallAudioState);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case USER_SWITCH_EARPIECE:
+                    mHasUserExplicitlyLeftBluetooth = true;
+                    // fall through
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mActiveEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                case USER_SWITCH_BLUETOOTH:
+                    // Nothing to do
+                    return HANDLED;
+                case USER_SWITCH_HEADSET:
+                    mHasUserExplicitlyLeftBluetooth = true;
+                    // fall through
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mActiveHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case USER_SWITCH_SPEAKER:
+                    mHasUserExplicitlyLeftBluetooth = true;
+                    // fall through
+                case SWITCH_SPEAKER:
+                    transitionTo(mActiveSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == NO_FOCUS) {
+                        reinitialize();
+                    } else if (msg.arg1 == ACTIVE_FOCUS) {
+                        transitionTo(mActiveBluetoothRoute);
+                    }
+                    return HANDLED;
+                case BT_AUDIO_DISCONNECT:
+                    // Ignore BT_AUDIO_DISCONNECT when ringing, since SCO audio should not be
+                    // connected.
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
     class QuiescentBluetoothRoute extends BluetoothRoute {
         @Override
         public String getName() {
@@ -678,6 +830,7 @@
             }
             switch (msg.what) {
                 case SWITCH_EARPIECE:
+                case USER_SWITCH_EARPIECE:
                     if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
                         transitionTo(mQuiescentEarpieceRoute);
                     } else {
@@ -685,9 +838,11 @@
                     }
                     return HANDLED;
                 case SWITCH_BLUETOOTH:
+                case USER_SWITCH_BLUETOOTH:
                     // Nothing to do
                     return HANDLED;
                 case SWITCH_HEADSET:
+                case USER_SWITCH_HEADSET:
                     if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
                         transitionTo(mQuiescentHeadsetRoute);
                     } else {
@@ -695,11 +850,14 @@
                     }
                     return HANDLED;
                 case SWITCH_SPEAKER:
+                case USER_SWITCH_SPEAKER:
                     transitionTo(mQuiescentSpeakerRoute);
                     return HANDLED;
                 case SWITCH_FOCUS:
-                    if (msg.arg1 == HAS_FOCUS) {
+                    if (msg.arg1 == ACTIVE_FOCUS) {
                         transitionTo(mActiveBluetoothRoute);
+                    } else if (msg.arg1 == RINGING_FOCUS) {
+                        transitionTo(mRingingBluetoothRoute);
                     }
                     return HANDLED;
                 case BT_AUDIO_DISCONNECT:
@@ -797,7 +955,8 @@
                     // fall through
                 case SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
-                        transitionTo(mActiveBluetoothRoute);
+                        transitionTo(mAudioFocusType == ACTIVE_FOCUS ?
+                                mActiveBluetoothRoute : mRingingBluetoothRoute);
                     } else {
                         Log.w(this, "Ignoring switch to bluetooth command. Not available.");
                     }
@@ -859,6 +1018,7 @@
             }
             switch(msg.what) {
                 case SWITCH_EARPIECE:
+                case USER_SWITCH_EARPIECE:
                     if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
                         transitionTo(mQuiescentEarpieceRoute);
                     } else {
@@ -866,6 +1026,7 @@
                     }
                     return HANDLED;
                 case SWITCH_BLUETOOTH:
+                case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
                         transitionTo(mQuiescentBluetoothRoute);
                     } else {
@@ -873,6 +1034,7 @@
                     }
                     return HANDLED;
                 case SWITCH_HEADSET:
+                case USER_SWITCH_HEADSET:
                     if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
                         transitionTo(mQuiescentHeadsetRoute);
                     } else {
@@ -880,10 +1042,11 @@
                     }
                     return HANDLED;
                 case SWITCH_SPEAKER:
+                case USER_SWITCH_SPEAKER:
                     // Nothing to do
                     return HANDLED;
                 case SWITCH_FOCUS:
-                    if (msg.arg1 == HAS_FOCUS) {
+                    if (msg.arg1 == ACTIVE_FOCUS || msg.arg1 == RINGING_FOCUS) {
                         transitionTo(mActiveSpeakerRoute);
                     }
                     return HANDLED;
@@ -939,6 +1102,7 @@
     private final ActiveHeadsetRoute mActiveHeadsetRoute = new ActiveHeadsetRoute();
     private final ActiveBluetoothRoute mActiveBluetoothRoute = new ActiveBluetoothRoute();
     private final ActiveSpeakerRoute mActiveSpeakerRoute = new ActiveSpeakerRoute();
+    private final RingingBluetoothRoute mRingingBluetoothRoute = new RingingBluetoothRoute();
     private final QuiescentEarpieceRoute mQuiescentEarpieceRoute = new QuiescentEarpieceRoute();
     private final QuiescentHeadsetRoute mQuiescentHeadsetRoute = new QuiescentHeadsetRoute();
     private final QuiescentBluetoothRoute mQuiescentBluetoothRoute = new QuiescentBluetoothRoute();
@@ -949,8 +1113,10 @@
      * states
      */
     private int mAvailableRoutes;
+    private int mAudioFocusType;
     private boolean mWasOnSpeaker;
     private boolean mIsMuted;
+    private boolean mAreNotificationSuppressed = false;
 
     private final Context mContext;
     private final CallsManager mCallsManager;
@@ -959,7 +1125,9 @@
     private final WiredHeadsetManager mWiredHeadsetManager;
     private final StatusBarNotifier mStatusBarNotifier;
     private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    private final InterruptionFilterProxy mInterruptionFilterProxy;
     private final boolean mDoesDeviceSupportEarpieceRoute;
+    private final TelecomSystem.SyncRoot mLock;
     private boolean mHasUserExplicitlyLeftBluetooth = false;
 
     private HashMap<String, Integer> mStateNameToRouteCode;
@@ -977,12 +1145,14 @@
             WiredHeadsetManager wiredHeadsetManager,
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
+            InterruptionFilterProxy interruptionFilterProxy,
             boolean doesDeviceSupportEarpieceRoute) {
         super(NAME);
         addState(mActiveEarpieceRoute);
         addState(mActiveHeadsetRoute);
         addState(mActiveBluetoothRoute);
         addState(mActiveSpeakerRoute);
+        addState(mRingingBluetoothRoute);
         addState(mQuiescentEarpieceRoute);
         addState(mQuiescentHeadsetRoute);
         addState(mQuiescentBluetoothRoute);
@@ -995,13 +1165,20 @@
         mWiredHeadsetManager = wiredHeadsetManager;
         mStatusBarNotifier = statusBarNotifier;
         mAudioServiceFactory = audioServiceFactory;
+        mInterruptionFilterProxy = interruptionFilterProxy;
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter =
+                new IntentFilter(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
+        context.registerReceiver(mReceiver, intentFilter);
         mDoesDeviceSupportEarpieceRoute = doesDeviceSupportEarpieceRoute;
+        mLock = callsManager.getLock();
 
         mStateNameToRouteCode = new HashMap<>(8);
         mStateNameToRouteCode.put(mQuiescentEarpieceRoute.getName(), ROUTE_EARPIECE);
         mStateNameToRouteCode.put(mQuiescentBluetoothRoute.getName(), ROUTE_BLUETOOTH);
         mStateNameToRouteCode.put(mQuiescentHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
         mStateNameToRouteCode.put(mQuiescentSpeakerRoute.getName(), ROUTE_SPEAKER);
+        mStateNameToRouteCode.put(mRingingBluetoothRoute.getName(), ROUTE_BLUETOOTH);
         mStateNameToRouteCode.put(mActiveEarpieceRoute.getName(), ROUTE_EARPIECE);
         mStateNameToRouteCode.put(mActiveBluetoothRoute.getName(), ROUTE_BLUETOOTH);
         mStateNameToRouteCode.put(mActiveHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
@@ -1100,10 +1277,54 @@
         quitNow();
     }
 
+    /**
+     * Sets whether notifications should be suppressed or not.  Used when in a call to ensure the
+     * device will not vibrate due to notifications.
+     * Alarm-only filtering is activated when
+     *
+     * @param on {@code true} when notification suppression should be activated, {@code false} when
+     *                       it should be deactivated.
+     */
+    private void setNotificationsSuppressed(boolean on) {
+        if (mInterruptionFilterProxy == null) {
+            return;
+        }
+
+        Log.i(this, "setNotificationsSuppressed: on=%s; suppressed=%s", (on ? "yes" : "no"),
+                (mAreNotificationSuppressed ? "yes" : "no"));
+        if (on) {
+            if (!mAreNotificationSuppressed) {
+                // Enabling suppression of notifications.
+                int interruptionFilter = mInterruptionFilterProxy.getCurrentInterruptionFilter();
+                if (interruptionFilter == NotificationManager.INTERRUPTION_FILTER_ALL) {
+                    // No interruption filter is specified, so suppress notifications by setting the
+                    // current filter to alarms-only.
+                    mAreNotificationSuppressed = true;
+                    mInterruptionFilterProxy.setInterruptionFilter(
+                            NotificationManager.INTERRUPTION_FILTER_ALARMS);
+                } else {
+                    // Interruption filter is already chosen by the user, so do not attempt to change
+                    // it.
+                    mAreNotificationSuppressed = false;
+                }
+            }
+        } else {
+            // Disabling suppression of notifications.
+            if (mAreNotificationSuppressed) {
+                // We have implemented the alarms-only policy and the user has not changed it since
+                // we originally set it, so reset the notification filter.
+                mInterruptionFilterProxy.setInterruptionFilter(
+                        NotificationManager.INTERRUPTION_FILTER_ALL);
+            }
+            mAreNotificationSuppressed = false;
+        }
+    }
+
     private void setSpeakerphoneOn(boolean on) {
         if (mAudioManager.isSpeakerphoneOn() != on) {
             Log.i(this, "turning speaker phone %s", on);
             mAudioManager.setSpeakerphoneOn(on);
+            mStatusBarNotifier.notifySpeakerphone(on);
         }
     }
 
@@ -1123,8 +1344,8 @@
 
     private void setMuteOn(boolean mute) {
         mIsMuted = mute;
-        Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
-                mute ? "on" : "off");
+        Log.event(mCallsManager.getForegroundCall(), mute ? Log.Events.MUTE : Log.Events.UNMUTE);
+
         if (mute != mAudioManager.isMicrophoneMute() && isInActiveState()) {
             IAudioService audio = mAudioServiceFactory.getAudioService();
             Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
@@ -1138,6 +1359,7 @@
                     // user and not the current foreground, which we want to avoid.
                     audio.setMicrophoneMute(
                             mute, mContext.getOpPackageName(), getCurrentUserId());
+                    mStatusBarNotifier.notifyMute(mute);
 
                 } catch (RemoteException e) {
                     Log.e(this, e, "Remote exception while toggling mute.");
@@ -1174,15 +1396,20 @@
     }
 
     private void setSystemAudioState(CallAudioState newCallAudioState, boolean force) {
-        Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
-                newCallAudioState);
-        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
-                CallAudioState.audioRouteToString(newCallAudioState.getRoute()));
+        synchronized (mLock) {
+            Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
+                    newCallAudioState);
+            if (force || !newCallAudioState.equals(mLastKnownCallAudioState)) {
+                if (newCallAudioState.getRoute() != mLastKnownCallAudioState.getRoute()) {
+                    Log.event(mCallsManager.getForegroundCall(),
+                            AUDIO_ROUTE_TO_LOG_EVENT.get(newCallAudioState.getRoute(),
+                                    Log.Events.AUDIO_ROUTE));
+                }
 
-        if (force || !newCallAudioState.equals(mLastKnownCallAudioState)) {
-            mCallsManager.onCallAudioStateChanged(mLastKnownCallAudioState, newCallAudioState);
-            updateAudioForForegroundCall(newCallAudioState);
-            mLastKnownCallAudioState = newCallAudioState;
+                mCallsManager.onCallAudioStateChanged(mLastKnownCallAudioState, newCallAudioState);
+                updateAudioForForegroundCall(newCallAudioState);
+                mLastKnownCallAudioState = newCallAudioState;
+            }
         }
     }
 
@@ -1301,6 +1528,7 @@
         setMuteOn(mIsMuted);
         mWasOnSpeaker = false;
         mHasUserExplicitlyLeftBluetooth = false;
+        mLastKnownCallAudioState = initState;
         transitionTo(mRouteCodeToQuiescentState.get(initState.getRoute()));
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
index b097bea..da2c199 100644
--- a/src/com/android/server/telecom/CallIdMapper.java
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -77,7 +77,16 @@
         }
     }
 
+    public interface ICallInfo {
+        String getCallId(Call call);
+    }
+
     private final BiMap<String, Call> mCalls = new BiMap<>();
+    private ICallInfo mCallInfo;
+
+    public CallIdMapper(ICallInfo callInfo) {
+        mCallInfo = callInfo;
+    }
 
     void replaceCall(Call newCall, Call callToReplace) {
         // Use the old call's ID for the new call.
@@ -93,7 +102,7 @@
     }
 
     void addCall(Call call) {
-        addCall(call, call.getId());
+        addCall(call, mCallInfo.getCallId(call));
     }
 
     void removeCall(Call call) {
@@ -111,7 +120,7 @@
         if (call == null || mCalls.getKey(call) == null) {
             return null;
         }
-        return call.getId();
+        return mCallInfo.getCallId(call);
     }
 
     Call getCall(Object objId) {
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index ffa76ce..1219216 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -148,7 +148,7 @@
             // process will be running throughout the duration of the phone call and should never
             // be killed.
             NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
-                    context, callsManager, call, intent, new PhoneNumberUtilsAdapterImpl(),
+                    context, callsManager, call, intent, callsManager.getPhoneNumberUtilsAdapter(),
                     isPrivilegedDialer);
             final int result = broadcaster.processIntent();
             final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 57b373b..64e266d 100755
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -144,15 +144,21 @@
         // 1) It was not in the "choose account" phase when disconnected
         // 2) It is a conference call
         // 3) Call was not explicitly canceled
+        // 4) Call is not an external call
         if (isNewlyDisconnected &&
                 (oldState != CallState.SELECT_PHONE_ACCOUNT &&
                  !call.isConference() &&
-                 !isCallCanceled)) {
+                 !isCallCanceled) &&
+                !call.isExternalCall()) {
             int type;
             if (!call.isIncoming()) {
                 type = Calls.OUTGOING_TYPE;
             } else if (disconnectCause == DisconnectCause.MISSED) {
                 type = Calls.MISSED_TYPE;
+            } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) {
+                type = Calls.ANSWERED_EXTERNALLY_TYPE;
+            } else if (disconnectCause == DisconnectCause.REJECTED) {
+                type = Calls.REJECTED_TYPE;
             } else {
                 type = Calls.INCOMING_TYPE;
             }
@@ -210,7 +216,8 @@
         Long callDataUsage = call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET ? null :
                 call.getCallDataUsage();
 
-        int callFeatures = getCallFeatures(call.getVideoStateHistory());
+        int callFeatures = getCallFeatures(call.getVideoStateHistory(),
+                call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED);
         logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber,
                 call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
                 creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser(),
@@ -283,13 +290,18 @@
      * Based on the video state of the call, determines the call features applicable for the call.
      *
      * @param videoState The video state.
+     * @param isPulledCall {@code true} if this call was pulled to another device.
      * @return The call features.
      */
-    private static int getCallFeatures(int videoState) {
+    private static int getCallFeatures(int videoState, boolean isPulledCall) {
+        int features = 0;
         if (VideoProfile.isVideo(videoState)) {
-            return Calls.FEATURES_VIDEO;
+            features |= Calls.FEATURES_VIDEO;
         }
-        return 0;
+        if (isPulledCall) {
+            features |= Calls.FEATURES_PULLED_EXTERNALLY;
+        }
+        return features;
     }
 
     /**
diff --git a/src/com/android/server/telecom/CallState.java b/src/com/android/server/telecom/CallState.java
index 0d2ca48..0aa928f 100644
--- a/src/com/android/server/telecom/CallState.java
+++ b/src/com/android/server/telecom/CallState.java
@@ -103,6 +103,15 @@
      */
     public static final int DISCONNECTING = 9;
 
+    /**
+     * Indicates that the call is in the process of being pulled to the local device.
+     * <p>
+     * This state should only be set on a call with
+     * {@link android.telecom.Connection#PROPERTY_IS_EXTERNAL_CALL} and
+     * {@link android.telecom.Connection#CAPABILITY_CAN_PULL_CALL}.
+     */
+    public static final int PULLING = 10;
+
     public static String toString(int callState) {
         switch (callState) {
             case NEW:
@@ -125,6 +134,8 @@
                 return "ABORTED";
             case DISCONNECTING:
                 return "DISCONNECTING";
+            case PULLING:
+                return "PULLING";
             default:
                 return "UNKNOWN";
         }
diff --git a/src/com/android/server/telecom/CallerInfoLookupHelper.java b/src/com/android/server/telecom/CallerInfoLookupHelper.java
index 4561c1c..244a802 100644
--- a/src/com/android/server/telecom/CallerInfoLookupHelper.java
+++ b/src/com/android/server/telecom/CallerInfoLookupHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
@@ -41,7 +42,7 @@
          * @param info
          * @return true if the value should be cached, false otherwise.
          */
-        void onCallerInfoQueryComplete(Uri handle, CallerInfo info);
+        void onCallerInfoQueryComplete(Uri handle, @Nullable CallerInfo info);
         void onContactPhotoQueryComplete(Uri handle, CallerInfo info);
     }
 
@@ -54,6 +55,7 @@
             listeners = new LinkedList<>();
         }
     }
+
     private final Map<Uri, CallerInfoQueryInfo> mQueryEntries = new HashMap<>();
 
     private final CallerInfoAsyncQueryFactory mCallerInfoAsyncQueryFactory;
@@ -74,11 +76,13 @@
 
     public void startLookup(final Uri handle, OnQueryCompleteListener listener) {
         if (handle == null) {
+            listener.onCallerInfoQueryComplete(handle, null);
             return;
         }
 
         final String number = handle.getSchemeSpecificPart();
         if (TextUtils.isEmpty(number)) {
+            listener.onCallerInfoQueryComplete(handle, null);
             return;
         }
 
@@ -93,7 +97,7 @@
                             info.callerInfo.cachedPhotoIcon != null)) {
                         listener.onContactPhotoQueryComplete(handle, info.callerInfo);
                     } else if (info.imageQueryPending) {
-                        Log.i(this, "There is a previously incomplete query for handle %s. " +
+                        Log.i(this, "There is a pending photo query for handle %s. " +
                                 "Adding to listeners for this query.", Log.piiHandle(handle));
                         info.listeners.add(listener);
                     }
@@ -110,7 +114,7 @@
             }
         }
 
-        mHandler.post(new Runnable("CILH.sL") {
+        mHandler.post(new Runnable("CILH.sL", mLock) {
             @Override
             public void loggedRun() {
                 Session continuedSession = Log.createSubsession();
@@ -137,11 +141,15 @@
                 Log.continueSession((Session) cookie, "CILH.oQC");
                 try {
                     if (mQueryEntries.containsKey(handle)) {
+                        Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed;" +
+                                " notifying all listeners.", Log.piiHandle(handle));
                         CallerInfoQueryInfo info = mQueryEntries.get(handle);
                         for (OnQueryCompleteListener l : info.listeners) {
                             l.onCallerInfoQueryComplete(handle, ci);
                         }
                         if (ci.contactDisplayPhotoUri == null) {
+                            Log.i(CallerInfoLookupHelper.this, "There is no photo for this " +
+                                    "contact, skipping photo query");
                             mQueryEntries.remove(handle);
                         } else {
                             info.callerInfo = ci;
@@ -150,7 +158,7 @@
                         }
                     } else {
                         Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed," +
-                                " but there are no listeners left.", handle);
+                                " but there are no listeners left.", Log.piiHandle(handle));
                     }
                 } finally {
                     Log.endSession();
@@ -160,7 +168,7 @@
     }
 
     private void startPhotoLookup(final Uri handle, final Uri contactPhotoUri) {
-        mHandler.post(new Runnable("CILH.sPL") {
+        mHandler.post(new Runnable("CILH.sPL", mLock) {
             @Override
             public void loggedRun() {
                 Session continuedSession = Log.createSubsession();
@@ -187,6 +195,7 @@
                         if (info.callerInfo == null) {
                             Log.w(CallerInfoLookupHelper.this, "Photo query finished, but the " +
                                     "CallerInfo object previously looked up was not cached.");
+                            mQueryEntries.remove(handle);
                             return;
                         }
                         info.callerInfo.cachedPhoto = photo;
@@ -197,7 +206,8 @@
                         mQueryEntries.remove(handle);
                     } else {
                         Log.i(CallerInfoLookupHelper.this, "Photo query for handle %s has" +
-                                " completed, but there are no listeners left.", handle);
+                                " completed, but there are no listeners left.",
+                                Log.piiHandle(handle));
                     }
                 } finally {
                     Log.endSession();
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 8d43842..5318f16 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom;
 
 import android.app.ActivityManager;
+import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.Intent;
@@ -69,6 +70,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -102,7 +104,7 @@
         void onRingbackRequested(Call call, boolean ringback);
         void onIsConferencedChanged(Call call);
         void onIsVoipAudioModeChanged(Call call);
-        void onVideoStateChanged(Call call);
+        void onVideoStateChanged(Call call, int previousVideoState, int newVideoState);
         void onCanAddCallChanged(boolean canAddCall);
         void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
         void onHoldToneRequested(Call call);
@@ -119,11 +121,13 @@
     private static final int MAXIMUM_TOP_LEVEL_CALLS = 2;
 
     private static final int[] OUTGOING_CALL_STATES =
-            {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING};
+            {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
+                    CallState.PULLING};
 
     private static final int[] LIVE_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
-                CallState.ACTIVE};
+                    CallState.PULLING, CallState.ACTIVE};
+
     public static final String TELECOM_CALL_ID_PREFIX = "TC@";
 
     // Maps call technologies in PhoneConstants to those in Analytics.
@@ -189,6 +193,8 @@
     private final CallerInfoLookupHelper mCallerInfoLookupHelper;
     private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
     private final Timeouts.Adapter mTimeoutsAdapter;
+    private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+    private final NotificationManager mNotificationManager;
     private final Set<Call> mLocallyDisconnectingCalls = new HashSet<>();
     private final Set<Call> mPendingCallsToDisconnect = new HashSet<>();
     /* Handler tied to thread in which CallManager was initialized. */
@@ -219,9 +225,12 @@
             SystemStateProvider systemStateProvider,
             DefaultDialerManagerAdapter defaultDialerAdapter,
             Timeouts.Adapter timeoutsAdapter,
-            AsyncRingtonePlayer asyncRingtonePlayer) {
+            AsyncRingtonePlayer asyncRingtonePlayer,
+            PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
+            InterruptionFilterProxy interruptionFilterProxy) {
         mContext = context;
         mLock = lock;
+        mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
         mContactsAsyncHelper = contactsAsyncHelper;
         mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
@@ -236,6 +245,8 @@
                 mContactsAsyncHelper, mLock);
 
         mDtmfLocalTonePlayer = new DtmfLocalTonePlayer();
+        mNotificationManager = (NotificationManager) context.getSystemService(
+                Context.NOTIFICATION_SERVICE);
         CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
                 context,
                 this,
@@ -243,6 +254,7 @@
                 wiredHeadsetManager,
                 statusBarNotifier,
                 audioServiceFactory,
+                interruptionFilterProxy,
                 CallAudioRouteStateMachine.doesDeviceSupportEarpieceRoute()
         );
         callAudioRouteStateMachine.initialize();
@@ -261,7 +273,7 @@
         RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
         SystemVibrator systemVibrator = new SystemVibrator(context);
         mInCallController = new InCallController(
-                context, mLock, this, systemStateProvider, defaultDialerAdapter);
+                context, mLock, this, systemStateProvider, defaultDialerAdapter, mTimeoutsAdapter);
         mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
                 ringtoneFactory, systemVibrator, mInCallController);
 
@@ -342,6 +354,12 @@
     @Override
     public void onSuccessfulIncomingCall(Call incomingCall) {
         Log.d(this, "onSuccessfulIncomingCall");
+        if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
+            Log.i(this, "Skipping call filtering due to ECBM");
+            onCallFilteringComplete(incomingCall, new CallFilteringResult(true, false, true, true));
+            return;
+        }
+
         List<IncomingCallFilter.CallFilter> filters = new ArrayList<>();
         filters.add(new DirectToVoicemailCallFilter(mCallerInfoLookupHelper));
         filters.add(new AsyncBlockCheckFilter(mContext, new BlockCheckerAdapter()));
@@ -363,6 +381,7 @@
                     result.shouldAllowCall ? "successful incoming call" : "blocking call");
         } else {
             Log.i(this, "onCallFilteringCompleted: call already disconnected.");
+            return;
         }
 
         if (result.shouldAllowCall) {
@@ -441,15 +460,12 @@
 
             mDtmfLocalTonePlayer.playTone(call, nextChar);
 
-            // TODO: Create a LockedRunnable class that does the synchronization automatically.
-            mStopTone = new Runnable("CM.oPDC") {
+            mStopTone = new Runnable("CM.oPDC", mLock) {
                 @Override
                 public void loggedRun() {
-                    synchronized (mLock) {
-                        // Set a timeout to stop the tone in case there isn't another tone to
-                        // follow.
-                        mDtmfLocalTonePlayer.stopTone(call);
-                    }
+                    // Set a timeout to stop the tone in case there isn't another tone to
+                    // follow.
+                    mDtmfLocalTonePlayer.stopTone(call);
                 }
             };
             mHandler.postDelayed(mStopTone.prepare(),
@@ -471,7 +487,7 @@
     @Override
     public void onParentChanged(Call call) {
         // parent-child relationship affects which call should be foreground, so do an update.
-        updateCallsManagerState();
+        updateCanAddCall();
         for (CallsManagerListener listener : mListeners) {
             listener.onIsConferencedChanged(call);
         }
@@ -480,7 +496,7 @@
     @Override
     public void onChildrenChanged(Call call) {
         // parent-child relationship affects which call should be foreground, so do an update.
-        updateCallsManagerState();
+        updateCanAddCall();
         for (CallsManagerListener listener : mListeners) {
             listener.onIsConferencedChanged(call);
         }
@@ -494,23 +510,21 @@
     }
 
     @Override
-    public void onVideoStateChanged(Call call) {
+    public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
         for (CallsManagerListener listener : mListeners) {
-            listener.onVideoStateChanged(call);
+            listener.onVideoStateChanged(call, previousVideoState, newVideoState);
         }
     }
 
     @Override
     public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call) {
         mPendingCallsToDisconnect.add(call);
-        mHandler.postDelayed(new Runnable("CM.oCVNOCB") {
+        mHandler.postDelayed(new Runnable("CM.oCVNOCB", mLock) {
             @Override
             public void loggedRun() {
-                synchronized (mLock) {
-                    if (mPendingCallsToDisconnect.remove(call)) {
-                        Log.i(this, "Delayed disconnection of call: %s", call);
-                        call.disconnect();
-                    }
+                if (mPendingCallsToDisconnect.remove(call)) {
+                    Log.i(this, "Delayed disconnection of call: %s", call);
+                    call.disconnect();
                 }
             }
         }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
@@ -625,7 +639,8 @@
         return false;
     }
 
-    CallAudioState getAudioState() {
+    @VisibleForTesting
+    public CallAudioState getAudioState() {
         return mCallAudioManager.getCallAudioState();
     }
 
@@ -668,6 +683,7 @@
                 mConnectionServiceRepository,
                 mContactsAsyncHelper,
                 mCallerInfoAsyncQueryFactory,
+                mPhoneNumberUtilsAdapter,
                 handle,
                 null /* gatewayInfo */,
                 null /* connectionManagerPhoneAccount */,
@@ -700,6 +716,7 @@
                 mConnectionServiceRepository,
                 mContactsAsyncHelper,
                 mCallerInfoAsyncQueryFactory,
+                mPhoneNumberUtilsAdapter,
                 handle,
                 null /* gatewayInfo */,
                 null /* connectionManagerPhoneAccount */,
@@ -735,9 +752,10 @@
         // Check to see if we can reuse any of the calls that are waiting to disconnect.
         // See {@link Call#abort} and {@link #onCanceledViaNewOutgoingCall} for more information.
         Call reusedCall = null;
-        for (Call pendingCall : mPendingCallsToDisconnect) {
+        for (Iterator<Call> callIter = mPendingCallsToDisconnect.iterator(); callIter.hasNext();) {
+            Call pendingCall = callIter.next();
             if (reusedCall == null && areHandlesEqual(pendingCall.getHandle(), handle)) {
-                mPendingCallsToDisconnect.remove(pendingCall);
+                callIter.remove();
                 Log.i(this, "Reusing disconnected call %s", pendingCall);
                 reusedCall = pendingCall;
             } else {
@@ -772,6 +790,7 @@
                     mConnectionServiceRepository,
                     mContactsAsyncHelper,
                     mCallerInfoAsyncQueryFactory,
+                    mPhoneNumberUtilsAdapter,
                     handle,
                     null /* gatewayInfo */,
                     null /* connectionManagerPhoneAccount */,
@@ -780,10 +799,10 @@
                     false /* forceAttachToExistingConnection */,
                     false /* isConference */
             );
-            call.setInitiatingUser(initiatingUser);
-
             call.initAnalytics();
 
+            call.setInitiatingUser(initiatingUser);
+
             isReusedCall = false;
         }
 
@@ -795,11 +814,13 @@
 
             // If this is an emergency video call, we need to check if the phone account supports
             // emergency video calling.
-            if (call.isEmergencyCall() && VideoProfile.isVideo(videoState)) {
+            // Also, ensure we don't try to place an outgoing call with video if video is not
+            // supported.
+            if (VideoProfile.isVideo(videoState)) {
                 PhoneAccount account =
                         mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
 
-                if (account != null &&
+                if (call.isEmergencyCall() && account != null &&
                         !account.hasCapabilities(PhoneAccount.CAPABILITY_EMERGENCY_VIDEO_CALLING)) {
                     // Phone account doesn't support emergency video calling, so fallback to
                     // audio-only now to prevent the InCall UI from setting up video surfaces
@@ -807,6 +828,12 @@
                     Log.i(this, "startOutgoingCall - emergency video calls not supported; " +
                             "falling back to audio-only");
                     videoState = VideoProfile.STATE_AUDIO_ONLY;
+                } else if (account != null &&
+                        !account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
+                    // Phone account doesn't support video calling, so fallback to audio-only.
+                    Log.i(this, "startOutgoingCall - video calls not supported; fallback to " +
+                            "audio-only.");
+                    videoState = VideoProfile.STATE_AUDIO_ONLY;
                 }
             }
 
@@ -825,7 +852,7 @@
             }
         }
 
-        if (phoneAccountHandle == null && accounts.size() > 0 && !call.isEmergencyCall()) {
+        if (phoneAccountHandle == null && accounts.size() > 0) {
             // No preset account, check if default exists that supports the URI scheme for the
             // handle and verify it can be used.
             if(accounts.size() > 1) {
@@ -924,26 +951,24 @@
 
         final boolean useSpeakerWhenDocked = mContext.getResources().getBoolean(
                 R.bool.use_speaker_when_docked);
-        final boolean isDocked = mDockManager.isDocked();
-        final boolean useSpeakerForVideoCall = isSpeakerphoneAutoEnabled(videoState);
+        final boolean useSpeakerForDock = isSpeakerphoneEnabledForDock();
+        final boolean useSpeakerForVideoCall = isSpeakerphoneAutoEnabledForVideoCalls(videoState);
 
         // Auto-enable speakerphone if the originating intent specified to do so, if the call
         // is a video call, of if using speaker when docked
         call.setStartWithSpeakerphoneOn(speakerphoneOn || useSpeakerForVideoCall
-                || (useSpeakerWhenDocked && isDocked));
+                || (useSpeakerWhenDocked && useSpeakerForDock));
         call.setVideoState(videoState);
 
         if (speakerphoneOn) {
             Log.i(this, "%s Starting with speakerphone as requested", call);
-        } else if (useSpeakerWhenDocked && useSpeakerWhenDocked) {
+        } else if (useSpeakerWhenDocked && useSpeakerForDock) {
             Log.i(this, "%s Starting with speakerphone because car is docked.", call);
         } else if (useSpeakerForVideoCall) {
             Log.i(this, "%s Starting with speakerphone because its a video call.", call);
         }
 
         if (call.isEmergencyCall()) {
-            // Emergency -- CreateConnectionProcessor will choose accounts automatically
-            call.setTargetPhoneAccount(null);
             new AsyncEmergencyContactNotifier(mContext).execute();
         }
 
@@ -993,7 +1018,8 @@
             // STATE_DIALING, put it on hold before answering the call.
             if (foregroundCall != null && foregroundCall != call &&
                     (foregroundCall.isActive() ||
-                     foregroundCall.getState() == CallState.DIALING)) {
+                     foregroundCall.getState() == CallState.DIALING ||
+                     foregroundCall.getState() == CallState.PULLING)) {
                 if (0 == (foregroundCall.getConnectionCapabilities()
                         & Connection.CAPABILITY_HOLD)) {
                     // This call does not support hold.  If it is from a different connection
@@ -1026,7 +1052,7 @@
             // We do not update the UI until we get confirmation of the answer() through
             // {@link #markCallAsActive}.
             call.answer(videoState);
-            if (isSpeakerphoneAutoEnabled(videoState)) {
+            if (isSpeakerphoneAutoEnabledForVideoCalls(videoState)) {
                 call.setStartWithSpeakerphoneOn(true);
             }
         }
@@ -1040,7 +1066,7 @@
      * @param videoState The video state of the call.
      * @return {@code true} if the speakerphone should be enabled.
      */
-    private boolean isSpeakerphoneAutoEnabled(int videoState) {
+    public boolean isSpeakerphoneAutoEnabledForVideoCalls(int videoState) {
         return VideoProfile.isVideo(videoState) &&
             !mWiredHeadsetManager.isPluggedIn() &&
             !mBluetoothManager.isBluetoothAvailable() &&
@@ -1048,6 +1074,19 @@
     }
 
     /**
+     * Determines if the speakerphone should be enabled for when docked.  Speakerphone
+     * should be enabled if the device is docked and bluetooth or the wired headset are
+     * not in use.
+     *
+     * @return {@code true} if the speakerphone should be enabled for the dock.
+     */
+    private boolean isSpeakerphoneEnabledForDock() {
+        return mDockManager.isDocked() &&
+            !mWiredHeadsetManager.isPluggedIn() &&
+            !mBluetoothManager.isBluetoothAvailable();
+    }
+
+    /**
      * Determines if the speakerphone should be automatically enabled for video calls.
      *
      * @return {@code true} if the speakerphone should automatically be enabled.
@@ -1168,13 +1207,19 @@
         if (!mCalls.contains(call)) {
             Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
         } else {
+            boolean otherCallHeld = false;
             Log.d(this, "unholding call: (%s)", call);
             for (Call c : mCalls) {
                 // Only attempt to hold parent calls and not the individual children.
                 if (c != null && c.isAlive() && c != call && c.getParentCall() == null) {
+                    otherCallHeld = true;
+                    Log.event(c, Log.Events.SWAP);
                     c.hold();
                 }
             }
+            if (otherCallHeld) {
+                Log.event(call, Log.Events.SWAP);
+            }
             call.unhold();
         }
     }
@@ -1186,6 +1231,7 @@
         }
         handleCallTechnologyChange(c);
         handleChildAddressChange(c);
+        updateCanAddCall();
     }
 
     // Construct the list of possible PhoneAccounts that the outgoing call can use based on the
@@ -1334,6 +1380,11 @@
         maybeMoveToSpeakerPhone(call);
     }
 
+    void markCallAsPulling(Call call) {
+        setCallState(call, CallState.PULLING, "pulling set explicitly");
+        maybeMoveToSpeakerPhone(call);
+    }
+
     void markCallAsActive(Call call) {
         setCallState(call, CallState.ACTIVE, "active set explicitly");
         maybeMoveToSpeakerPhone(call);
@@ -1359,12 +1410,21 @@
      */
     void markCallAsRemoved(Call call) {
         removeCall(call);
+        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
         if (mLocallyDisconnectingCalls.contains(call)) {
             mLocallyDisconnectingCalls.remove(call);
-            Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
             if (foregroundCall != null && foregroundCall.getState() == CallState.ON_HOLD) {
                 foregroundCall.unhold();
             }
+        } else if (foregroundCall != null &&
+                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD)  &&
+                foregroundCall.getState() == CallState.ON_HOLD) {
+
+            // The new foreground call is on hold, however the carrier does not display the hold
+            // button in the UI.  Therefore, we need to auto unhold the held call since the user has
+            // no means of unholding it themselves.
+            Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)");
+            foregroundCall.unhold();
         }
     }
 
@@ -1421,13 +1481,14 @@
                     mCallAudioManager.toggleMute();
                     return true;
                 } else {
-                    ringingCall.answer(ringingCall.getVideoState());
+                    ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
                     return true;
                 }
             } else if (HeadsetMediaButton.LONG_PRESS == type) {
                 Log.d(this, "handleHeadsetHook: longpress -> hangup");
                 Call callToHangup = getFirstCallWithState(
-                        CallState.RINGING, CallState.DIALING, CallState.ACTIVE, CallState.ON_HOLD);
+                        CallState.RINGING, CallState.DIALING, CallState.PULLING, CallState.ACTIVE,
+                        CallState.ON_HOLD);
                 if (callToHangup != null) {
                     callToHangup.disconnect();
                     return true;
@@ -1440,7 +1501,8 @@
     /**
      * Returns true if telecom supports adding another top-level call.
      */
-    boolean canAddCall() {
+    @VisibleForTesting
+    public boolean canAddCall() {
         boolean isDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.DEVICE_PROVISIONED, 0) != 0;
         if (!isDeviceProvisioned) {
@@ -1457,9 +1519,18 @@
             if (call.isEmergencyCall()) {
                 // We never support add call if one of the calls is an emergency call.
                 return false;
+            } else if (call.isExternalCall()) {
+                // External calls don't count.
+                continue;
             } else if (call.getParentCall() == null) {
                 count++;
             }
+            Bundle extras = call.getExtras();
+            if (extras != null) {
+                if (extras.getBoolean(Connection.EXTRA_DISABLE_ADD_CALL, false)) {
+                    return false;
+                }
+            }
 
             // We do not check states for canAddCall. We treat disconnected calls the same
             // and wait until they are removed instead. If we didn't count disconnected calls,
@@ -1470,6 +1541,7 @@
                 return false;
             }
         }
+
         return true;
     }
 
@@ -1513,6 +1585,11 @@
         return getFirstCallWithState(null, states);
     }
 
+    @VisibleForTesting
+    public PhoneNumberUtilsAdapter getPhoneNumberUtilsAdapter() {
+        return mPhoneNumberUtilsAdapter;
+    }
+
     /**
      * Returns the first call that it finds with the given states. The states are treated as having
      * priority order so that any call with the first state will be returned before any call with
@@ -1538,6 +1615,10 @@
                     continue;
                 }
 
+                if (call.isExternalCall()) {
+                    continue;
+                }
+
                 if (currentState == call.getState()) {
                     return call;
                 }
@@ -1566,6 +1647,7 @@
                 mConnectionServiceRepository,
                 mContactsAsyncHelper,
                 mCallerInfoAsyncQueryFactory,
+                mPhoneNumberUtilsAdapter,
                 null /* handle */,
                 null /* gatewayInfo */,
                 null /* connectionManagerPhoneAccount */,
@@ -1619,7 +1701,16 @@
      * @param incomingCall Incoming call that has been rejected
      */
     private void rejectCallAndLog(Call incomingCall) {
-        incomingCall.reject(false, null);
+        if (incomingCall.getConnectionService() != null) {
+            // Only reject the call if it has not already been destroyed.  If a call ends while
+            // incoming call filtering is taking place, it is possible that the call has already
+            // been destroyed, and as such it will be impossible to send the reject to the
+            // associated ConnectionService.
+            incomingCall.reject(false, null);
+        } else {
+            Log.i(this, "rejectCallAndLog - call already destroyed.");
+        }
+
         // Since the call was not added to the list of calls, we have to call the missed
         // call notifier and the call logger manually.
         // Do we need missed call notification for direct to Voicemail calls?
@@ -1644,7 +1735,7 @@
         extras.putLong(TelecomManager.EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS,
                 SystemClock.elapsedRealtime());
 
-        updateCallsManagerState();
+        updateCanAddCall();
         // onCallAdded for calls which immediately take the foreground (like the first call).
         for (CallsManagerListener listener : mListeners) {
             if (Log.SYSTRACE_DEBUG) {
@@ -1676,7 +1767,7 @@
 
         // Only broadcast changes for calls that are being tracked.
         if (shouldNotify) {
-            updateCallsManagerState();
+            updateCanAddCall();
             for (CallsManagerListener listener : mListeners) {
                 if (Log.SYSTRACE_DEBUG) {
                     Trace.beginSection(listener.getClass().toString() + " onCallRemoved");
@@ -1717,7 +1808,7 @@
             Trace.beginSection("onCallStateChanged");
             // Only broadcast state change for calls that are being tracked.
             if (mCalls.contains(call)) {
-                updateCallsManagerState();
+                updateCanAddCall();
                 for (CallsManagerListener listener : mListeners) {
                     if (Log.SYSTRACE_DEBUG) {
                         Trace.beginSection(listener.getClass().toString() + " onCallStateChanged");
@@ -1748,10 +1839,6 @@
         }
     }
 
-    private void updateCallsManagerState() {
-        updateCanAddCall();
-    }
-
     private boolean isPotentialMMICode(Uri handle) {
         return (handle != null && handle.getSchemeSpecificPart() != null
                 && handle.getSchemeSpecificPart().contains("#"));
@@ -1787,7 +1874,9 @@
         int count = 0;
         for (int state : states) {
             for (Call call : mCalls) {
-                if (call.getParentCall() == null && call.getState() == state) {
+                if (call.getParentCall() == null && call.getState() == state &&
+                        !call.isExternalCall()) {
+
                     count++;
                 }
             }
@@ -1812,7 +1901,7 @@
     }
 
     private boolean hasMaximumDialingCalls() {
-        return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(CallState.DIALING);
+        return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(CallState.DIALING, CallState.PULLING);
     }
 
     private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
@@ -1950,6 +2039,8 @@
      * @return The new call.
      */
     Call createCallForExistingConnection(String callId, ParcelableConnection connection) {
+        boolean isDowngradedConference = (connection.getConnectionProperties()
+                & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0;
         Call call = new Call(
                 callId,
                 mContext,
@@ -1958,13 +2049,14 @@
                 mConnectionServiceRepository,
                 mContactsAsyncHelper,
                 mCallerInfoAsyncQueryFactory,
+                mPhoneNumberUtilsAdapter,
                 connection.getHandle() /* handle */,
                 null /* gatewayInfo */,
                 null /* connectionManagerPhoneAccount */,
                 connection.getPhoneAccount(), /* targetPhoneAccountHandle */
                 Call.CALL_DIRECTION_UNDEFINED /* callDirection */,
                 false /* forceAttachToExistingConnection */,
-                false /* isConference */,
+                isDowngradedConference /* isConference */,
                 connection.getConnectTimeMillis() /* connectTimeMillis */);
 
         call.initAnalytics();
@@ -2016,6 +2108,10 @@
         }
     }
 
+    public TelecomSystem.SyncRoot getLock() {
+        return mLock;
+    }
+
     private void reloadMissedCallsOfUser(UserHandle userHandle) {
         mMissedCallNotifier.reloadFromDatabase(mCallerInfoLookupHelper,
                 new MissedCallNotifier.CallInfoFactory(), userHandle);
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 9bfa098..a4c76c1 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -69,7 +69,7 @@
     }
 
     @Override
-    public void onVideoStateChanged(Call call) {
+    public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
     }
 
     @Override
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 563d9e1..bf82a99 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.app.AppOpsManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.net.Uri;
@@ -32,6 +33,7 @@
 import android.telecom.GatewayInfo;
 import android.telecom.ParcelableConference;
 import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.StatusHints;
 import android.telecom.TelecomManager;
@@ -66,7 +68,7 @@
         @Override
         public void handleCreateConnectionComplete(String callId, ConnectionRequest request,
                 ParcelableConnection connection) {
-            Log.startSession("CSW.hCCC");
+            Log.startSession(Log.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -82,7 +84,7 @@
 
         @Override
         public void setActive(String callId) {
-            Log.startSession("CSW.sA");
+            Log.startSession(Log.Sessions.CSW_SET_ACTIVE);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -102,7 +104,7 @@
 
         @Override
         public void setRinging(String callId) {
-            Log.startSession("CSW.sR");
+            Log.startSession(Log.Sessions.CSW_SET_RINGING);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -140,7 +142,7 @@
 
         @Override
         public void setDialing(String callId) {
-            Log.startSession("CSW.sD");
+            Log.startSession(Log.Sessions.CSW_SET_DIALING);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -159,8 +161,26 @@
         }
 
         @Override
+        public void setPulling(String callId) {
+            Log.startSession(Log.Sessions.CSW_SET_PULLING);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    logIncoming("setPulling %s", callId);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        mCallsManager.markCallAsPulling(call);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
+
+        @Override
         public void setDisconnected(String callId, DisconnectCause disconnectCause) {
-            Log.startSession("CSW.sD");
+            Log.startSession(Log.Sessions.CSW_SET_DISCONNECTED);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -181,7 +201,7 @@
 
         @Override
         public void setOnHold(String callId) {
-            Log.startSession("CSW.sOH");
+            Log.startSession(Log.Sessions.CSW_SET_ON_HOLD);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -221,7 +241,7 @@
 
         @Override
         public void removeCall(String callId) {
-            Log.startSession("CSW.rC");
+            Log.startSession(Log.Sessions.CSW_REMOVE_CALL);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -283,7 +303,7 @@
 
         @Override
         public void setIsConferenced(String callId, String conferenceCallId) {
-            Log.startSession("CSW.sIC");
+            Log.startSession(Log.Sessions.CSW_SET_IS_CONFERENCED);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -319,15 +339,7 @@
                     // deliver the message anyway that they want. b/20530631.
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        // Just refresh the connection capabilities so that the UI
-                        // is forced to reenable the merge button as the capability
-                        // is still on the connection. Note when b/20530631 is fixed, we need
-                        // to revisit this fix to remove this hacky way of unhiding the merge
-                        // button (side effect of reprocessing the capabilities) and plumb
-                        // the failure event all the way to InCallUI instead of stopping
-                        // it here. That way we can also handle the UI of notifying that
-                        // the merged has failed.
-                        call.setConnectionCapabilities(call.getConnectionCapabilities(), true);
+                        call.onConnectionEvent(Connection.EVENT_CALL_MERGE_FAILED, null);
                     } else {
                         Log.w(this, "setConferenceMergeFailed, unknown call id: %s", callId);
                     }
@@ -340,7 +352,7 @@
 
         @Override
         public void addConferenceCall(String callId, ParcelableConference parcelableConference) {
-            Log.startSession("CSW.aCC");
+            Log.startSession(Log.Sessions.CSW_ADD_CONFERENCE_CALL);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -609,14 +621,39 @@
         @Override
         public void addExistingConnection(String callId, ParcelableConnection connection) {
             Log.startSession("CSW.aEC");
+            UserHandle userHandle = Binder.getCallingUserHandle();
+            // Check that the Calling Package matches PhoneAccountHandle's Component Package
+            PhoneAccountHandle callingPhoneAccountHandle = connection.getPhoneAccount();
+            if (callingPhoneAccountHandle != null) {
+                mAppOpsManager.checkPackage(Binder.getCallingUid(),
+                        callingPhoneAccountHandle.getComponentName().getPackageName());
+            }
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    logIncoming("addExistingConnection  %s %s", callId, connection);
-                    Call existingCall = mCallsManager
-                            .createCallForExistingConnection(callId, connection);
-                    mCallIdMapper.addCall(existingCall, callId);
-                    existingCall.setConnectionService(ConnectionServiceWrapper.this);
+                    // Make sure that the PhoneAccount associated with the incoming
+                    // ParcelableConnection is in fact registered to Telecom and is being called
+                    // from the correct user.
+                    List<PhoneAccountHandle> accountHandles =
+                            mPhoneAccountRegistrar.getCallCapablePhoneAccounts(null /*uriScheme*/,
+                                    false /*includeDisabledAccounts*/, userHandle);
+                    PhoneAccountHandle phoneAccountHandle = null;
+                    for (PhoneAccountHandle accountHandle : accountHandles) {
+                        if(accountHandle.equals(callingPhoneAccountHandle)) {
+                            phoneAccountHandle = accountHandle;
+                        }
+                    }
+                    if (phoneAccountHandle != null) {
+                        logIncoming("addExistingConnection  %s %s", callId, connection);
+                        Call existingCall = mCallsManager
+                                .createCallForExistingConnection(callId, connection);
+                        mCallIdMapper.addCall(existingCall, callId);
+                        existingCall.setConnectionService(ConnectionServiceWrapper.this);
+                    } else {
+                        Log.e(this, new RemoteException("The PhoneAccount being used is not " +
+                                "currently registered with Telecom."), "Unable to " +
+                                "addExistingConnection.");
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -644,7 +681,7 @@
     }
 
     private final Adapter mAdapter = new Adapter();
-    private final CallIdMapper mCallIdMapper = new CallIdMapper();
+    private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getConnectionId);
     private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();
 
     private Binder2 mBinder = new Binder2();
@@ -652,6 +689,7 @@
     private final ConnectionServiceRepository mConnectionServiceRepository;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final CallsManager mCallsManager;
+    private final AppOpsManager mAppOpsManager;
 
     /**
      * Creates a connection service.
@@ -679,6 +717,7 @@
         });
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mCallsManager = callsManager;
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
     }
 
     /** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -692,6 +731,17 @@
         }
     }
 
+    /** See {@link IConnectionService#removeConnectionServiceAdapter}. */
+    private void removeConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
+        if (isServiceValid("removeConnectionServiceAdapter")) {
+            try {
+                logOutgoing("removeConnectionServiceAdapter %s", adapter);
+                mServiceInterface.removeConnectionServiceAdapter(adapter);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
     /**
      * Creates a new connection for a new outgoing call or to attach to an existing incoming call.
      */
@@ -1011,18 +1061,23 @@
     /** {@inheritDoc} */
     @Override
     protected void setServiceInterface(IBinder binder) {
-        if (binder == null) {
-            // We have lost our service connection. Notify the world that this service is done.
-            // We must notify the adapter before CallsManager. The adapter will force any pending
-            // outgoing calls to try the next service. This needs to happen before CallsManager
-            // tries to clean up any calls still associated with this service.
-            handleConnectionServiceDeath();
-            mCallsManager.handleConnectionServiceDeath(this);
-            mServiceInterface = null;
-        } else {
-            mServiceInterface = IConnectionService.Stub.asInterface(binder);
-            addConnectionServiceAdapter(mAdapter);
-        }
+        mServiceInterface = IConnectionService.Stub.asInterface(binder);
+        Log.v(this, "Adding Connection Service Adapter.");
+        addConnectionServiceAdapter(mAdapter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void removeServiceInterface() {
+        Log.v(this, "Removing Connection Service Adapter.");
+        removeConnectionServiceAdapter(mAdapter);
+        // We have lost our service connection. Notify the world that this service is done.
+        // We must notify the adapter before CallsManager. The adapter will force any pending
+        // outgoing calls to try the next service. This needs to happen before CallsManager
+        // tries to clean up any calls still associated with this service.
+        handleConnectionServiceDeath();
+        mCallsManager.handleConnectionServiceDeath(this);
+        mServiceInterface = null;
     }
 
     private void handleCreateConnectionComplete(
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 7f9bbab..22ec3c6 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -96,6 +96,7 @@
     private final Context mContext;
     private CreateConnectionTimeout mTimeout;
     private ConnectionServiceWrapper mService;
+    private int mConnectionAttempt;
 
     @VisibleForTesting
     public CreateConnectionProcessor(
@@ -107,6 +108,7 @@
         mCallResponse = response;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mContext = context;
+        mConnectionAttempt = 0;
     }
 
     boolean isProcessingComplete() {
@@ -117,6 +119,10 @@
         return mTimeout != null && mTimeout.isCallTimedOut();
     }
 
+    public int getConnectionAttempt() {
+        return mConnectionAttempt;
+    }
+
     @VisibleForTesting
     public void process() {
         Log.v(this, "process");
@@ -127,7 +133,7 @@
                     mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
         }
         adjustAttemptsForConnectionManager();
-        adjustAttemptsForEmergency();
+        adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
         mAttemptRecordIterator = mAttemptRecords.iterator();
         attemptNextPhoneAccount();
     }
@@ -200,6 +206,7 @@
                 Log.i(this, "Found no connection service for attempt %s", attempt);
                 attemptNextPhoneAccount();
             } else {
+                mConnectionAttempt++;
                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
                 mCall.setConnectionService(mService);
@@ -288,7 +295,7 @@
 
     // If we are possibly attempting to call a local emergency number, ensure that the
     // plain PSTN connection services are listed, and nothing else.
-    private void adjustAttemptsForEmergency() {
+    private void adjustAttemptsForEmergency(PhoneAccountHandle preferredPAH) {
         if (mCall.isEmergencyCall()) {
             Log.i(this, "Emergency number detected");
             mAttemptRecords.clear();
@@ -307,16 +314,29 @@
                 allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
             }
 
-            // First, add SIM phone accounts which can place emergency calls.
+            // First, possibly add the SIM phone account that the user prefers
+            PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                    preferredPAH);
+            if (preferredPA != null &&
+                    preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
+                    preferredPA.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+                Log.i(this, "Will try PSTN account %s for emergency",
+                        preferredPA.getAccountHandle());
+                mAttemptRecords.add(new CallAttemptRecord(preferredPAH, preferredPAH));
+            }
+
+            // Next, add all SIM phone accounts which can place emergency calls.
+            TelephonyUtil.sortSimPhoneAccounts(mContext, allAccounts);
             for (PhoneAccount phoneAccount : allAccounts) {
                 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
                         phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-                    Log.i(this, "Will try PSTN account %s for emergency",
-                            phoneAccount.getAccountHandle());
-                    mAttemptRecords.add(
-                            new CallAttemptRecord(
-                                    phoneAccount.getAccountHandle(),
-                                    phoneAccount.getAccountHandle()));
+                    PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
+                    // Don't add the preferred account since it has already been added previously.
+                    if (!phoneAccountHandle.equals(preferredPAH)) {
+                        Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle);
+                        mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
+                                phoneAccountHandle));
+                    }
                 }
             }
 
@@ -419,7 +439,7 @@
     public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
         // Failure of some sort; record the reasons for failure and try again if possible
         Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
-        if(shouldFailCallIfConnectionManagerFails(errorDisconnectCause)){
+        if (shouldFailCallIfConnectionManagerFails(errorDisconnectCause)) {
             notifyCallConnectionFailure(errorDisconnectCause);
             return;
         }
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 69cc129..8bc3373 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -40,7 +40,7 @@
 
     CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
             ConnectionServiceWrapper service, Call call) {
-        super("CCT");
+        super("CCT", null /*lock*/);
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mConnectionService = service;
@@ -114,7 +114,8 @@
         int state = call.getState();
         return state == CallState.NEW
             || state == CallState.CONNECTING
-            || state == CallState.DIALING;
+            || state == CallState.DIALING
+            || state == CallState.PULLING;
     }
 
     private long getTimeoutLengthMillis() {
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index a8bd354..af0ce13 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -135,6 +135,9 @@
     /** ${inheritDoc} */
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         if (!mCallsManager.hasAnyCalls()) {
             mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
         }
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 31d268e..e47f3a2 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -47,7 +47,7 @@
     @Override
     public void answerCall(String callId, int videoState) {
         try {
-            Log.startSession("ICA.aC", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_ANSWER_CALL, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -70,7 +70,7 @@
     @Override
     public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) {
         try {
-            Log.startSession("ICA.rC", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_REJECT_CALL, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -162,7 +162,7 @@
     @Override
     public void disconnectCall(String callId) {
         try {
-            Log.startSession("ICA.dC", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_DISCONNECT_CALL, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -185,7 +185,7 @@
     @Override
     public void holdCall(String callId) {
         try {
-            Log.startSession("ICA.hC", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_HOLD_CALL, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -207,7 +207,7 @@
     @Override
     public void unholdCall(String callId) {
         try {
-            Log.startSession("ICA.uC", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_UNHOLD_CALL, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -252,7 +252,7 @@
     @Override
     public void mute(boolean shouldMute) {
         try {
-            Log.startSession("ICA.m", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_MUTE, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -269,7 +269,7 @@
     @Override
     public void setAudioRoute(int route) {
         try {
-            Log.startSession("ICA.sAR", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_SET_AUDIO_ROUTE, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
@@ -286,7 +286,7 @@
     @Override
     public void conference(String callId, String otherCallId) {
         try {
-            Log.startSession("ICA.c", mOwnerComponentName);
+            Log.startSession(Log.Sessions.ICA_CONFERENCE, mOwnerComponentName);
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 0a14ffb..2157bf3 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -78,6 +78,60 @@
         public void dump(IndentingPrintWriter pw) {}
     }
 
+    private class InCallServiceInfo {
+        private final ComponentName mComponentName;
+        private boolean mIsExternalCallsSupported;
+        private final int mType;
+
+        public InCallServiceInfo(ComponentName componentName,
+                boolean isExternalCallsSupported,
+                int type) {
+            mComponentName = componentName;
+            mIsExternalCallsSupported = isExternalCallsSupported;
+            mType = type;
+        }
+
+        public ComponentName getComponentName() {
+            return mComponentName;
+        }
+
+        public boolean isExternalCallsSupported() {
+            return mIsExternalCallsSupported;
+        }
+
+        public int getType() {
+            return mType;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            InCallServiceInfo that = (InCallServiceInfo) o;
+
+            if (mIsExternalCallsSupported != that.mIsExternalCallsSupported) {
+                return false;
+            }
+            return mComponentName.equals(that.mComponentName);
+
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mComponentName, mIsExternalCallsSupported);
+        }
+
+        @Override
+        public String toString() {
+            return "[" + mComponentName + " supportsExternal? " + mIsExternalCallsSupported + "]";
+        }
+    }
+
     private class InCallServiceBindingConnection extends InCallServiceConnection {
 
         private final ServiceConnection mServiceConnection = new ServiceConnection() {
@@ -113,12 +167,12 @@
             }
         };
 
-        private final ComponentName mComponentName;
+        private final InCallServiceInfo mInCallServiceInfo;
         private boolean mIsConnected = false;
         private boolean mIsBound = false;
 
-        public InCallServiceBindingConnection(ComponentName componentName) {
-            mComponentName = componentName;
+        public InCallServiceBindingConnection(InCallServiceInfo info) {
+            mInCallServiceInfo = info;
         }
 
         @Override
@@ -129,7 +183,7 @@
             }
 
             Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
-            intent.setComponent(mComponentName);
+            intent.setComponent(mInCallServiceInfo.getComponentName());
             if (call != null && !call.isIncoming() && !call.isExternalCall()){
                 intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
                         call.getIntentExtras());
@@ -137,7 +191,7 @@
                         call.getTargetPhoneAccount());
             }
 
-            Log.i(this, "Attempting to bind to InCall %s, with %s", mComponentName, intent);
+            Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
             mIsConnected = true;
             if (!mContext.bindServiceAsUser(intent, mServiceConnection,
                         Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
@@ -146,6 +200,12 @@
                 mIsConnected = false;
             }
 
+            if (call != null && mIsConnected) {
+                call.getAnalytics().addInCallService(
+                        mInCallServiceInfo.getComponentName().flattenToShortString(),
+                        mInCallServiceInfo.getType());
+            }
+
             return mIsConnected;
         }
 
@@ -168,10 +228,10 @@
 
         protected void onConnected(IBinder service) {
             boolean shouldRemainConnected =
-                    InCallController.this.onConnected(mComponentName, service);
+                    InCallController.this.onConnected(mInCallServiceInfo, service);
             if (!shouldRemainConnected) {
                 // Sometimes we can opt to disconnect for certain reasons, like if the
-                // InCallService rejected our intialization step, or the calls went away
+                // InCallService rejected our initialization step, or the calls went away
                 // in the time it took us to bind to the InCallService. In such cases, we go
                 // ahead and disconnect ourselves.
                 disconnect();
@@ -179,7 +239,7 @@
         }
 
         protected void onDisconnected() {
-            InCallController.this.onDisconnected(mComponentName);
+            InCallController.this.onDisconnected(mInCallServiceInfo.getComponentName());
             disconnect();  // Unbind explicitly if we get disconnected.
             if (mListener != null) {
                 mListener.onDisconnect(InCallServiceBindingConnection.this);
@@ -214,8 +274,9 @@
         };
 
         public EmergencyInCallServiceConnection(
-                ComponentName componentName, InCallServiceConnection subConnection) {
-            super(componentName);
+                InCallServiceInfo info, InCallServiceConnection subConnection) {
+
+            super(info);
             mSubConnection = subConnection;
             if (mSubConnection != null) {
                 mSubConnection.setListener(mSubListener);
@@ -499,7 +560,7 @@
         }
 
         @Override
-        public void onVideoStateChanged(Call call) {
+        public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
             updateCall(call);
         }
 
@@ -535,14 +596,14 @@
     private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
 
     /** The in-call app implementations, see {@link IInCallService}. */
-    private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
+    private final Map<InCallServiceInfo, IInCallService> mInCallServices = new ArrayMap<>();
 
     /**
      * The {@link ComponentName} of the bound In-Call UI Service.
      */
     private ComponentName mInCallUIComponentName;
 
-    private final CallIdMapper mCallIdMapper = new CallIdMapper();
+    private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
 
     /** The {@link ComponentName} of the default InCall UI. */
     private final ComponentName mSystemInCallComponentName;
@@ -552,17 +613,19 @@
     private final CallsManager mCallsManager;
     private final SystemStateProvider mSystemStateProvider;
     private final DefaultDialerManagerAdapter mDefaultDialerAdapter;
+    private final Timeouts.Adapter mTimeoutsAdapter;
     private CarSwappingInCallServiceConnection mInCallServiceConnection;
     private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections;
 
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
             SystemStateProvider systemStateProvider,
-            DefaultDialerManagerAdapter defaultDialerAdapter) {
+            DefaultDialerManagerAdapter defaultDialerAdapter, Timeouts.Adapter timeoutsAdapter) {
         mContext = context;
         mLock = lock;
         mCallsManager = callsManager;
         mSystemStateProvider = systemStateProvider;
         mDefaultDialerAdapter = defaultDialerAdapter;
+        mTimeoutsAdapter = timeoutsAdapter;
 
         Resources resources = mContext.getResources();
         mSystemInCallComponentName = new ComponentName(
@@ -583,16 +646,26 @@
             // Track the call if we don't already know about it.
             addCall(call);
 
-            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
-                ComponentName componentName = entry.getKey();
+            List<ComponentName> componentsUpdated = new ArrayList<>();
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
+                }
+
+                componentsUpdated.add(info.getComponentName());
                 IInCallService inCallService = entry.getValue();
+
                 ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
-                        true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar());
+                        true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
+                        info.isExternalCallsSupported());
                 try {
                     inCallService.addCall(parcelableCall);
                 } catch (RemoteException ignored) {
                 }
             }
+            Log.i(this, "Call added to components: %s", componentsUpdated);
         }
     }
 
@@ -604,17 +677,15 @@
              *  give them enough time to process all the pending messages.
              */
             Handler handler = new Handler(Looper.getMainLooper());
-            handler.postDelayed(new Runnable("ICC.oCR") {
+            handler.postDelayed(new Runnable("ICC.oCR", mLock) {
                 @Override
                 public void loggedRun() {
-                    synchronized (mLock) {
-                        // Check again to make sure there are no active calls.
-                        if (mCallsManager.getCalls().isEmpty()) {
-                            unbindFromServices();
-                        }
+                    // Check again to make sure there are no active calls.
+                    if (mCallsManager.getCalls().isEmpty()) {
+                        unbindFromServices();
                     }
                 }
-            }.prepare(), Timeouts.getCallRemoveUnbindInCallServicesDelay(
+            }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
                             mContext.getContentResolver()));
         }
         call.removeListener(mCallListener);
@@ -624,8 +695,62 @@
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
-        // TODO: Need to add logic which ensures changes to a call's external state adds or removes
-        // the call from the InCallServices depending on whether they support external calls.
+
+        List<ComponentName> componentsUpdated = new ArrayList<>();
+        if (!isExternalCall) {
+            // The call was external but it is no longer external.  We must now add it to any
+            // InCallServices which do not support external calls.
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+
+                if (info.isExternalCallsSupported()) {
+                    // For InCallServices which support external calls, the call will have already
+                    // been added to the connection service, so we do not need to add it again.
+                    continue;
+                }
+
+                componentsUpdated.add(info.getComponentName());
+                IInCallService inCallService = entry.getValue();
+
+                ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
+                        true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
+                        info.isExternalCallsSupported());
+                try {
+                    inCallService.addCall(parcelableCall);
+                } catch (RemoteException ignored) {
+                }
+            }
+            Log.i(this, "Previously external call added to components: %s", componentsUpdated);
+        } else {
+            // The call was regular but it is now external.  We must now remove it from any
+            // InCallServices which do not support external calls.
+            // Remove the call by sending a call update indicating the call was disconnected.
+            ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
+                    call,
+                    false /* includeVideoProvider */,
+                    mCallsManager.getPhoneAccountRegistrar(),
+                    false /* supportsExternalCalls */,
+                    android.telecom.Call.STATE_DISCONNECTED /* overrideState */);
+
+            Log.i(this, "Removing external call %s ==> %s", call, parcelableCall);
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+                if (info.isExternalCallsSupported()) {
+                    // For InCallServices which support external calls, we do not need to remove
+                    // the call.
+                    continue;
+                }
+
+                componentsUpdated.add(info.getComponentName());
+                IInCallService inCallService = entry.getValue();
+
+                try {
+                    inCallService.updateCall(parcelableCall);
+                } catch (RemoteException ignored) {
+                }
+            }
+            Log.i(this, "External call removed from components: %s", componentsUpdated);
+        }
     }
 
     @Override
@@ -715,6 +840,10 @@
         if (!mInCallServices.isEmpty()) {
             for (IInCallService inCallService : mInCallServices.values()) {
                 try {
+                    Log.i(this, "notifyConnectionEvent {Call: %s, Event: %s, Extras:[%s]}",
+                            (call != null ? call.toString() :"null"),
+                            (event != null ? event : "null") ,
+                            (extras != null ? extras.toString() : "null"));
                     inCallService.onConnectionEvent(mCallIdMapper.getCallId(call), event, extras);
                 } catch (RemoteException ignored) {
                 }
@@ -727,10 +856,14 @@
      */
     private void unbindFromServices() {
         if (isBoundToServices()) {
-            mInCallServiceConnection.disconnect();
-            mInCallServiceConnection = null;
-            mNonUIInCallServiceConnections.disconnect();
-            mNonUIInCallServiceConnections = null;
+            if (mInCallServiceConnection != null) {
+                mInCallServiceConnection.disconnect();
+                mInCallServiceConnection = null;
+            }
+            if (mNonUIInCallServiceConnections != null) {
+                mNonUIInCallServiceConnections.disconnect();
+                mNonUIInCallServiceConnections = null;
+            }
         }
     }
 
@@ -743,23 +876,25 @@
     @VisibleForTesting
     public void bindToServices(Call call) {
         InCallServiceConnection dialerInCall = null;
-        ComponentName defaultDialerComponent = getDefaultDialerComponent();
-        Log.i(this, "defaultDialer: " + defaultDialerComponent);
-        if (defaultDialerComponent != null &&
-                !defaultDialerComponent.equals(mSystemInCallComponentName)) {
-            dialerInCall = new InCallServiceBindingConnection(defaultDialerComponent);
+        InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent();
+        Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
+        if (defaultDialerComponentInfo != null &&
+                !defaultDialerComponentInfo.getComponentName().equals(mSystemInCallComponentName)) {
+            dialerInCall = new InCallServiceBindingConnection(defaultDialerComponentInfo);
         }
         Log.i(this, "defaultDialer: " + dialerInCall);
 
+        InCallServiceInfo systemInCallInfo =  getInCallServiceComponent(mSystemInCallComponentName,
+                IN_CALL_SERVICE_TYPE_SYSTEM_UI);
         EmergencyInCallServiceConnection systemInCall =
-                new EmergencyInCallServiceConnection(mSystemInCallComponentName, dialerInCall);
+                new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
         systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
 
         InCallServiceConnection carModeInCall = null;
-        ComponentName carModeComponent = getCarModeComponent();
-        if (carModeComponent != null &&
-                !carModeComponent.equals(mSystemInCallComponentName)) {
-            carModeInCall = new InCallServiceBindingConnection(carModeComponent);
+        InCallServiceInfo carModeComponentInfo = getCarModeComponent();
+        if (carModeComponentInfo != null &&
+                !carModeComponentInfo.getComponentName().equals(mSystemInCallComponentName)) {
+            carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
         }
 
         mInCallServiceConnection =
@@ -767,18 +902,17 @@
         mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
         mInCallServiceConnection.connect(call);
 
-
-        List<ComponentName> nonUIInCallComponents =
-                getInCallServiceComponents(null, IN_CALL_SERVICE_TYPE_NON_UI);
+        List<InCallServiceInfo> nonUIInCallComponents =
+                getInCallServiceComponents(IN_CALL_SERVICE_TYPE_NON_UI);
         List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
-        for (ComponentName componentName : nonUIInCallComponents) {
-            nonUIInCalls.add(new InCallServiceBindingConnection(componentName));
+        for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
+            nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
         }
         mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(nonUIInCalls);
         mNonUIInCallServiceConnections.connect(call);
     }
 
-    private ComponentName getDefaultDialerComponent() {
+    private InCallServiceInfo getDefaultDialerComponent() {
         String packageName = mDefaultDialerAdapter.getDefaultDialerApplication(
                 mContext, mCallsManager.getCurrentUserHandle().getIdentifier());
         Log.d(this, "Default Dialer package: " + packageName);
@@ -786,25 +920,57 @@
         return getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_DIALER_UI);
     }
 
-    private ComponentName getCarModeComponent() {
-        return getInCallServiceComponent(null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
+    private InCallServiceInfo getCarModeComponent() {
+        // Seems strange to cast a String to null, but the signatures of getInCallServiceComponent
+        // differ in the types of the first parameter, and passing in null is inherently ambiguous.
+        return getInCallServiceComponent((String) null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
     }
 
-    private ComponentName getInCallServiceComponent(String packageName, int type) {
-        List<ComponentName> list = getInCallServiceComponents(packageName, type);
+    private InCallServiceInfo getInCallServiceComponent(ComponentName componentName, int type) {
+        List<InCallServiceInfo> list = getInCallServiceComponents(componentName, type);
+        if (list != null && !list.isEmpty()) {
+            return list.get(0);
+        } else {
+            // Last Resort: Try to bind to the ComponentName given directly.
+            Log.e(this, new Exception(), "Package Manager could not find ComponentName: "
+                    + componentName +". Trying to bind anyway.");
+            return new InCallServiceInfo(componentName, false, type);
+        }
+    }
+
+    private InCallServiceInfo getInCallServiceComponent(String packageName, int type) {
+        List<InCallServiceInfo> list = getInCallServiceComponents(packageName, type);
         if (list != null && !list.isEmpty()) {
             return list.get(0);
         }
         return null;
     }
 
-    private List<ComponentName> getInCallServiceComponents(String packageName, int type) {
-        List<ComponentName> retval = new LinkedList<>();
+    private List<InCallServiceInfo> getInCallServiceComponents(int type) {
+        return getInCallServiceComponents(null, null, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(String packageName, int type) {
+        return getInCallServiceComponents(packageName, null, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(ComponentName componentName,
+            int type) {
+        return getInCallServiceComponents(null, componentName, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(String packageName,
+            ComponentName componentName, int requestedType) {
+
+        List<InCallServiceInfo> retval = new LinkedList<>();
 
         Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
         if (packageName != null) {
             serviceIntent.setPackage(packageName);
         }
+        if (componentName != null) {
+            serviceIntent.setComponent(componentName);
+        }
 
         PackageManager packageManager = mContext.getPackageManager();
         for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
@@ -814,8 +980,15 @@
             ServiceInfo serviceInfo = entry.serviceInfo;
 
             if (serviceInfo != null) {
-                if (type == 0 || type == getInCallServiceType(entry.serviceInfo, packageManager)) {
-                    retval.add(new ComponentName(serviceInfo.packageName, serviceInfo.name));
+                boolean isExternalCallsSupported = serviceInfo.metaData != null &&
+                        serviceInfo.metaData.getBoolean(
+                                TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, false);
+                if (requestedType == 0 || requestedType == getInCallServiceType(entry.serviceInfo,
+                        packageManager)) {
+
+                    retval.add(new InCallServiceInfo(
+                            new ComponentName(serviceInfo.packageName, serviceInfo.name),
+                            isExternalCallsSupported, requestedType));
                 }
             }
         }
@@ -861,7 +1034,6 @@
             return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
         }
 
-
         // Check to see that it is the default dialer package
         boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
                 mDefaultDialerAdapter.getDefaultDialerApplication(
@@ -899,16 +1071,16 @@
      * this class and in-call app by sending the first update to in-call app. This method is
      * called after a successful binding connection is established.
      *
-     * @param componentName The service {@link ComponentName}.
+     * @param info Info about the service, including its {@link ComponentName}.
      * @param service The {@link IInCallService} implementation.
      * @return True if we successfully connected.
      */
-    private boolean onConnected(ComponentName componentName, IBinder service) {
-        Trace.beginSection("onConnected: " + componentName);
-        Log.i(this, "onConnected to %s", componentName);
+    private boolean onConnected(InCallServiceInfo info, IBinder service) {
+        Trace.beginSection("onConnected: " + info.getComponentName());
+        Log.i(this, "onConnected to %s", info.getComponentName());
 
         IInCallService inCallService = IInCallService.Stub.asInterface(service);
-        mInCallServices.put(componentName, inCallService);
+        mInCallServices.put(info, inCallService);
 
         try {
             inCallService.setInCallAdapter(
@@ -916,7 +1088,7 @@
                             mCallsManager,
                             mCallIdMapper,
                             mLock,
-                            componentName.getPackageName()));
+                            info.getComponentName().getPackageName()));
         } catch (RemoteException e) {
             Log.e(this, e, "Failed to set the in-call adapter.");
             Trace.endSection();
@@ -925,28 +1097,32 @@
 
         // Upon successful connection, send the state of the world to the service.
         List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
-        if (!calls.isEmpty()) {
-            Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
-                    componentName);
-            for (Call call : calls) {
-                try {
-                    // Track the call if we don't already know about it.
-                    addCall(call);
-                    inCallService.addCall(ParcelableCallUtils.toParcelableCall(
-                            call,
-                            true /* includeVideoProvider */,
-                            mCallsManager.getPhoneAccountRegistrar()));
-                } catch (RemoteException ignored) {
-                }
-            }
+        Log.i(this, "Adding %s calls to InCallService after onConnected: %s, including external " +
+                "calls", calls.size(), info.getComponentName());
+        int numCallsSent = 0;
+        for (Call call : calls) {
             try {
-                inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
-                inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
+                }
+
+                // Track the call if we don't already know about it.
+                addCall(call);
+                numCallsSent += 1;
+                inCallService.addCall(ParcelableCallUtils.toParcelableCall(
+                        call,
+                        true /* includeVideoProvider */,
+                        mCallsManager.getPhoneAccountRegistrar(),
+                        info.isExternalCallsSupported()));
             } catch (RemoteException ignored) {
             }
-        } else {
-            return false;
         }
+        try {
+            inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
+            inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
+        } catch (RemoteException ignored) {
+        }
+        Log.i(this, "%s calls sent to InCallService.", numCallsSent);
         Trace.endSection();
         return true;
     }
@@ -980,16 +1156,23 @@
      */
     private void updateCall(Call call, boolean videoProviderChanged) {
         if (!mInCallServices.isEmpty()) {
-            ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
-                    call,
-                    videoProviderChanged /* includeVideoProvider */,
-                    mCallsManager.getPhoneAccountRegistrar());
-            Log.i(this, "Sending updateCall %s ==> %s", call, parcelableCall);
+            Log.i(this, "Sending updateCall %s", call);
             List<ComponentName> componentsUpdated = new ArrayList<>();
-            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
-                ComponentName componentName = entry.getKey();
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
+                }
+
+                ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
+                        call,
+                        videoProviderChanged /* includeVideoProvider */,
+                        mCallsManager.getPhoneAccountRegistrar(),
+                        info.isExternalCallsSupported());
+                ComponentName componentName = info.getComponentName();
                 IInCallService inCallService = entry.getValue();
                 componentsUpdated.add(componentName);
+
                 try {
                     inCallService.updateCall(parcelableCall);
                 } catch (RemoteException ignored) {
@@ -1022,8 +1205,8 @@
     public void dump(IndentingPrintWriter pw) {
         pw.println("mInCallServices (InCalls registered):");
         pw.increaseIndent();
-        for (ComponentName componentName : mInCallServices.keySet()) {
-            pw.println(componentName);
+        for (InCallServiceInfo info : mInCallServices.keySet()) {
+            pw.println(info);
         }
         pw.decreaseIndent();
 
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 0b0e78b..e0b0dc0 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -273,7 +273,12 @@
             mSession = Log.createSubsession();
         }
 
-        start();
+        super.start();
+    }
+
+    @Override
+    public void start() {
+        Log.w(this, "Do not call the start method directly; use startTone instead.");
     }
 
     /**
@@ -292,15 +297,13 @@
 
     private void cleanUpTonePlayer() {
         // Release focus on the main thread.
-        mMainThreadHandler.post(new Runnable("ICTP.cUTP") {
+        mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) {
             @Override
             public void loggedRun() {
-                synchronized (mLock) {
-                    if (sTonesPlaying == 0) {
-                        Log.wtf(this, "Over-releasing focus for tone player.");
-                    } else if (--sTonesPlaying == 0) {
-                        mCallAudioManager.setIsTonePlaying(false);
-                    }
+                if (sTonesPlaying == 0) {
+                    Log.wtf(this, "Over-releasing focus for tone player.");
+                } else if (--sTonesPlaying == 0) {
+                    mCallAudioManager.setIsTonePlaying(false);
                 }
             }
         }.prepare());
diff --git a/src/com/android/server/telecom/InCallWakeLockController.java b/src/com/android/server/telecom/InCallWakeLockController.java
index ffa6a3f..0de8123 100644
--- a/src/com/android/server/telecom/InCallWakeLockController.java
+++ b/src/com/android/server/telecom/InCallWakeLockController.java
@@ -37,16 +37,25 @@
 
     @Override
     public void onCallAdded(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
diff --git a/src/com/android/server/telecom/InterruptionFilterProxy.java b/src/com/android/server/telecom/InterruptionFilterProxy.java
new file mode 100644
index 0000000..434c341
--- /dev/null
+++ b/src/com/android/server/telecom/InterruptionFilterProxy.java
@@ -0,0 +1,27 @@
+/*
+ * 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.server.telecom;
+
+/**
+ * Defines common functionality used by {@link CallAudioRouteStateMachine} to control the current
+ * interruption filter for notifications while in a call.  Used to ensure that this functionality
+ * can be mocked out in unit tests.
+ */
+public interface InterruptionFilterProxy {
+    void setInterruptionFilter(int interruptionFilter);
+    int getCurrentInterruptionFilter();
+}
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
index fda21fd..2282ff0 100644
--- a/src/com/android/server/telecom/Log.java
+++ b/src/com/android/server/telecom/Log.java
@@ -22,6 +22,7 @@
 import android.os.Looper;
 import android.os.AsyncTask;
 import android.telecom.PhoneAccount;
+import android.telecom.TimedEvent;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Base64;
@@ -34,7 +35,9 @@
 import java.security.NoSuchAlgorithmException;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.IllegalFormatException;
@@ -52,16 +55,61 @@
 @VisibleForTesting
 public class Log {
 
+    public static final class Sessions {
+        public static final String ICA_ANSWER_CALL = "ICA.aC";
+        public static final String ICA_REJECT_CALL = "ICA.rC";
+        public static final String ICA_DISCONNECT_CALL = "ICA.dC";
+        public static final String ICA_HOLD_CALL = "ICA.hC";
+        public static final String ICA_UNHOLD_CALL = "ICA.uC";
+        public static final String ICA_MUTE = "ICA.m";
+        public static final String ICA_SET_AUDIO_ROUTE = "ICA.sAR";
+        public static final String ICA_CONFERENCE = "ICA.c";
+        public static final String CSW_HANDLE_CREATE_CONNECTION_COMPLETE = "CSW.hCCC";
+        public static final String CSW_SET_ACTIVE = "CSW.sA";
+        public static final String CSW_SET_RINGING = "CSW.sR";
+        public static final String CSW_SET_DIALING = "CSW.sD";
+        public static final String CSW_SET_PULLING = "CSW.sP";
+        public static final String CSW_SET_DISCONNECTED = "CSW.sDc";
+        public static final String CSW_SET_ON_HOLD = "CSW.sOH";
+        public static final String CSW_REMOVE_CALL = "CSW.rC";
+        public static final String CSW_SET_IS_CONFERENCED = "CSW.sIC";
+        public static final String CSW_ADD_CONFERENCE_CALL = "CSW.aCC";
+    }
+
     /**
      * Stores the various events associated with {@link Call}s. Also stores all request-response
      * pairs amongst the events.
      */
     public final static class Events {
+        public static class TimedEventPair {
+            private static final long DEFAULT_TIMEOUT = 3000L;
+
+            String mRequest;
+            String mResponse;
+            String mName;
+            long mTimeoutMillis = DEFAULT_TIMEOUT;
+
+            public TimedEventPair(String request, String response,
+                    String name) {
+                this.mRequest = request;
+                this.mResponse = response;
+                this.mName = name;
+            }
+
+            public TimedEventPair(String request, String response,
+                    String name, long timeoutMillis) {
+                this.mRequest = request;
+                this.mResponse = response;
+                this.mName = name;
+                this.mTimeoutMillis = timeoutMillis;
+            }
+        }
+
         public static final String CREATED = "CREATED";
         public static final String DESTROYED = "DESTROYED";
-        public static final String SET_NEW = "SET_NEW";
         public static final String SET_CONNECTING = "SET_CONNECTING";
         public static final String SET_DIALING = "SET_DIALING";
+        public static final String SET_PULLING = "SET_PULLING";
         public static final String SET_ACTIVE = "SET_ACTIVE";
         public static final String SET_HOLD = "SET_HOLD";
         public static final String SET_RINGING = "SET_RINGING";
@@ -84,13 +132,18 @@
         public static final String BIND_CS = "BIND_CS";
         public static final String CS_BOUND = "CS_BOUND";
         public static final String CONFERENCE_WITH = "CONF_WITH";
-        public static final String SPLIT_CONFERENCE = "CONF_SPLIT";
+        public static final String SPLIT_FROM_CONFERENCE = "CONF_SPLIT";
         public static final String SWAP = "SWAP";
         public static final String ADD_CHILD = "ADD_CHILD";
         public static final String REMOVE_CHILD = "REMOVE_CHILD";
         public static final String SET_PARENT = "SET_PARENT";
         public static final String MUTE = "MUTE";
+        public static final String UNMUTE = "UNMUTE";
         public static final String AUDIO_ROUTE = "AUDIO_ROUTE";
+        public static final String AUDIO_ROUTE_EARPIECE = "AUDIO_ROUTE_EARPIECE";
+        public static final String AUDIO_ROUTE_HEADSET = "AUDIO_ROUTE_HEADSET";
+        public static final String AUDIO_ROUTE_BT = "AUDIO_ROUTE_BT";
+        public static final String AUDIO_ROUTE_SPEAKER = "AUDIO_ROUTE_SPEAKER";
         public static final String ERROR_LOG = "ERROR";
         public static final String USER_LOG_MARK = "USER_LOG_MARK";
         public static final String SILENCE = "SILENCE";
@@ -107,29 +160,71 @@
         public static final String FILTERING_TIMED_OUT = "FILTERING_TIMED_OUT";
         public static final String REMOTELY_HELD = "REMOTELY_HELD";
         public static final String REMOTELY_UNHELD = "REMOTELY_UNHELD";
-        public static final String PULL = "PULL";
+        public static final String REQUEST_PULL = "PULL";
         public static final String INFO = "INFO";
+        public static final String VIDEO_STATE_CHANGED = "VIDEO_STATE_CHANGED";
+        public static final String RECEIVE_VIDEO_REQUEST = "RECEIVE_VIDEO_REQUEST";
+        public static final String RECEIVE_VIDEO_RESPONSE = "RECEIVE_VIDEO_RESPONSE";
+        public static final String SEND_VIDEO_REQUEST = "SEND_VIDEO_REQUEST";
+        public static final String SEND_VIDEO_RESPONSE = "SEND_VIDEO_RESPONSE";
+        public static final String IS_EXTERNAL = "IS_EXTERNAL";
+        public static final String PROPERTY_CHANGE = "PROPERTY_CHANGE";
+        public static final String CAPABILITY_CHANGE = "CAPABILITY_CHANGE";
+        public static final String CONNECTION_EVENT = "CONNECTION_EVENT";
+
+        public static class Timings {
+            public static final String ACCEPT_TIMING = "accept";
+            public static final String REJECT_TIMING = "reject";
+            public static final String DISCONNECT_TIMING = "disconnect";
+            public static final String HOLD_TIMING = "hold";
+            public static final String UNHOLD_TIMING = "unhold";
+            public static final String OUTGOING_TIME_TO_DIALING_TIMING = "outgoing_time_to_dialing";
+            public static final String BIND_CS_TIMING = "bind_cs";
+            public static final String SCREENING_COMPLETED_TIMING = "screening_completed";
+            public static final String DIRECT_TO_VM_FINISHED_TIMING = "direct_to_vm_finished";
+            public static final String BLOCK_CHECK_FINISHED_TIMING = "block_check_finished";
+            public static final String FILTERING_COMPLETED_TIMING = "filtering_completed";
+            public static final String FILTERING_TIMED_OUT_TIMING = "filtering_timed_out";
+
+            private static final TimedEventPair[] sTimedEvents = {
+                    new TimedEventPair(REQUEST_ACCEPT, SET_ACTIVE, ACCEPT_TIMING),
+                    new TimedEventPair(REQUEST_REJECT, SET_DISCONNECTED, REJECT_TIMING),
+                    new TimedEventPair(REQUEST_DISCONNECT, SET_DISCONNECTED, DISCONNECT_TIMING),
+                    new TimedEventPair(REQUEST_HOLD, SET_HOLD, HOLD_TIMING),
+                    new TimedEventPair(REQUEST_UNHOLD, SET_ACTIVE, UNHOLD_TIMING),
+                    new TimedEventPair(START_CONNECTION, SET_DIALING,
+                            OUTGOING_TIME_TO_DIALING_TIMING),
+                    new TimedEventPair(BIND_CS, CS_BOUND, BIND_CS_TIMING),
+                    new TimedEventPair(SCREENING_SENT, SCREENING_COMPLETED,
+                            SCREENING_COMPLETED_TIMING),
+                    new TimedEventPair(DIRECT_TO_VM_INITIATED, DIRECT_TO_VM_FINISHED,
+                            DIRECT_TO_VM_FINISHED_TIMING),
+                    new TimedEventPair(BLOCK_CHECK_INITIATED, BLOCK_CHECK_FINISHED,
+                            BLOCK_CHECK_FINISHED_TIMING),
+                    new TimedEventPair(FILTERING_INITIATED, FILTERING_COMPLETED,
+                            FILTERING_COMPLETED_TIMING),
+                    new TimedEventPair(FILTERING_INITIATED, FILTERING_TIMED_OUT,
+                            FILTERING_TIMED_OUT_TIMING, 6000L),
+            };
+        }
 
         /**
-         * Maps from a request to a response.  The same event could be listed as the
-         * response for multiple requests (e.g. REQUEST_ACCEPT and REQUEST_UNHOLD both map to the
-         * SET_ACTIVE response). This map is used to print out the amount of time it takes between
-         * a request and a response.
+         * Maps from request events to a list of possible response events. Used to track
+         * end-to-end timing for critical user-facing operations in Telecom.
          */
-        public static final Map<String, String> requestResponsePairs =
-                new HashMap<String, String>() {{
-                    put(REQUEST_ACCEPT, SET_ACTIVE);
-                    put(REQUEST_REJECT, SET_DISCONNECTED);
-                    put(REQUEST_DISCONNECT, SET_DISCONNECTED);
-                    put(REQUEST_HOLD, SET_HOLD);
-                    put(REQUEST_UNHOLD, SET_ACTIVE);
-                    put(START_CONNECTION, SET_DIALING);
-                    put(BIND_CS, CS_BOUND);
-                    put(SCREENING_SENT, SCREENING_COMPLETED);
-                    put(BLOCK_CHECK_INITIATED, BLOCK_CHECK_FINISHED);
-                    put(DIRECT_TO_VM_INITIATED, DIRECT_TO_VM_FINISHED);
-                    put(FILTERING_INITIATED, FILTERING_COMPLETED);
-                }};
+        public static final Map<String, List<TimedEventPair>> requestResponsePairs;
+        static {
+            requestResponsePairs = new HashMap<>();
+            for (TimedEventPair p : Timings.sTimedEvents) {
+                if (requestResponsePairs.containsKey(p.mRequest)) {
+                    requestResponsePairs.get(p.mRequest).add(p);
+                } else {
+                    ArrayList<TimedEventPair> responses = new ArrayList<>();
+                    responses.add(p);
+                    requestResponsePairs.put(p.mRequest, responses);
+                }
+            }
+        }
     }
 
     public static class CallEvent {
@@ -147,10 +242,42 @@
     }
 
     public static class CallEventRecord {
+        public static class EventTiming extends TimedEvent<String> {
+            public String name;
+            public long time;
+
+            public EventTiming(String name, long time) {
+                this.name = name;
+                this.time = time;
+            }
+
+            public String getKey() {
+                return name;
+            }
+
+            public long getTime() {
+                return time;
+            }
+        }
+
+        private static class PendingResponse {
+            String requestEventId;
+            long requestEventTimeMillis;
+            long timeoutMillis;
+            String name;
+
+            public PendingResponse(String requestEventId, long requestEventTimeMillis,
+                    long timeoutMillis, String name) {
+                this.requestEventId = requestEventId;
+                this.requestEventTimeMillis = requestEventTimeMillis;
+                this.timeoutMillis = timeoutMillis;
+                this.name = name;
+            }
+        }
+
         private static final DateFormat sLongDateFormat = new SimpleDateFormat(
                 "yyyy-MM-dd HH:mm:ss.SSS");
         private static final DateFormat sDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
-        private static int sNextId = 1;
         private final List<CallEvent> mEvents = new LinkedList<>();
         private final Call mCall;
 
@@ -167,9 +294,40 @@
             Log.i("Event", "Call %s: %s, %s", mCall.getId(), event, data);
         }
 
-        public void dump(IndentingPrintWriter pw) {
-            Map<String, CallEvent> pendingResponses = new HashMap<>();
+        public List<CallEvent> getEvents() {
+            return mEvents;
+        }
 
+        public List<EventTiming> extractEventTimings() {
+            if (mEvents == null) {
+                return Collections.emptyList();
+            }
+
+            LinkedList<EventTiming> result = new LinkedList<>();
+            Map<String, PendingResponse> pendingResponses = new HashMap<>();
+            for (CallEvent event : mEvents) {
+                if (Events.requestResponsePairs.containsKey(event.eventId)) {
+                    // This event expects a response, so add that expected response to the maps
+                    // of pending events.
+                    for (Events.TimedEventPair p : Events.requestResponsePairs.get(event.eventId)) {
+                        pendingResponses.put(p.mResponse, new PendingResponse(event.eventId,
+                                event.time, p.mTimeoutMillis, p.mName));
+                    }
+                }
+
+                PendingResponse pendingResponse = pendingResponses.remove(event.eventId);
+                if (pendingResponse != null) {
+                    long elapsedTime = event.time - pendingResponse.requestEventTimeMillis;
+                    if (elapsedTime < pendingResponse.timeoutMillis) {
+                        result.add(new EventTiming(pendingResponse.name, elapsedTime));
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        public void dump(IndentingPrintWriter pw) {
             pw.print("Call ");
             pw.print(mCall.getId());
             pw.print(" [");
@@ -181,20 +339,6 @@
             pw.println("To address: " + piiHandle(mCall.getHandle()));
 
             for (CallEvent event : mEvents) {
-
-                // We print out events in chronological order. During that process we look at each
-                // event and see if it maps to a request on the Request-Response pairs map. If it
-                // does, then we effectively start 'listening' for the response. We do that by
-                // storing the response event ID in {@code pendingResponses}. When we find the
-                // response in a later iteration of the loop, we grab the original request and
-                // calculate the time it took to get a response.
-                if (Events.requestResponsePairs.containsKey(event.eventId)) {
-                    // This event expects a response, so add that response to the maps
-                    // of pending events.
-                    String pendingResponse = Events.requestResponsePairs.get(event.eventId);
-                    pendingResponses.put(pendingResponse, event);
-                }
-
                 pw.print(sDateFormat.format(new Date(event.time)));
                 pw.print(" - ");
                 pw.print(event.eventId);
@@ -214,26 +358,25 @@
                     pw.print(data);
                     pw.print(")");
                 }
-
-                // If this event is a response event that we've been waiting for, calculate the time
-                // it took for the response to complete and print that out as well.
-                CallEvent requestEvent = pendingResponses.remove(event.eventId);
-                if (requestEvent != null) {
-                    pw.print(", time since ");
-                    pw.print(requestEvent.eventId);
-                    pw.print(": ");
-                    pw.print(event.time - requestEvent.time);
-                    pw.print(" ms");
-                }
                 pw.print(":");
                 pw.print(event.sessionId);
                 pw.println();
             }
+
+            pw.println("Timings (average for this call, milliseconds):");
+            pw.increaseIndent();
+            Map<String, Double> avgEventTimings = EventTiming.averageTimings(extractEventTimings());
+            List<String> eventNames = new ArrayList<>(avgEventTimings.keySet());
+            Collections.sort(eventNames);
+            for (String eventName : eventNames) {
+                pw.printf("%s: %.2f\n", eventName, avgEventTimings.get(eventName));
+            }
+            pw.decreaseIndent();
             pw.decreaseIndent();
         }
     }
 
-    public static final int MAX_CALLS_TO_CACHE = 5;  // Arbitrarily chosen.
+    public static final int MAX_CALLS_TO_CACHE = 10;  // Arbitrarily chosen.
     public static final int MAX_CALLS_TO_CACHE_DEBUG = 20;  // Arbitrarily chosen.
     private static final long EXTENDED_LOGGING_DURATION_MILLIS = 60000 * 30; // 30 minutes
 
@@ -548,6 +691,7 @@
             // running time of the session.
             long fullSessionTimeMs =
                     System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
+            Analytics.addSessionTiming(subsession.getShortMethodName(), fullSessionTimeMs);
             Log.v(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs + " ms): " +
                     subsession.toString());
         }
@@ -601,6 +745,20 @@
         }
     }
 
+    public static void event(Call call, String event, String format, Object... args) {
+        String msg;
+        try {
+            msg = (args == null || args.length == 0) ? format
+                    : String.format(Locale.US, format, args);
+        } catch (IllegalFormatException ife) {
+            e("Log", ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
+                    args.length);
+            msg = format + " (An error occurred while formatting the message.)";
+        }
+
+        event(call, event, msg);
+    }
+
     @VisibleForTesting
     public static synchronized void cleanupStaleSessions(long timeoutMs) {
         String logMessage = "Stale Sessions Cleaned:\n";
@@ -641,6 +799,13 @@
         // Now add a new entry
         mCallEventRecords.add(newRecord);
         mCallEventRecordMap.put(call, newRecord);
+
+        // Register the events with Analytics
+        if (call.getAnalytics() != null) {
+            call.getAnalytics().setCallEvents(newRecord);
+        } else {
+            Log.w(LOGGING_TAG, "Call analytics is null");
+        }
     }
 
     /**
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 4d3fb84..8b5604b 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -55,9 +55,6 @@
  */
 @VisibleForTesting
 public class NewOutgoingCallIntentBroadcaster {
-    private static final String EXTRA_ACTUAL_NUMBER_TO_DIAL =
-            "android.telecom.extra.ACTUAL_NUMBER_TO_DIAL";
-
     /**
      * Legacy string constants used to retrieve gateway provider extras from intents. These still
      * need to be copied from the source call intent to the destination intent in order to
@@ -67,14 +64,13 @@
     public static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
             "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
     public static final String EXTRA_GATEWAY_URI = "com.android.phone.extra.GATEWAY_URI";
-    public static final String EXTRA_GATEWAY_ORIGINAL_URI =
-            "com.android.phone.extra.GATEWAY_ORIGINAL_URI";
 
     private final CallsManager mCallsManager;
     private final Call mCall;
     private final Intent mIntent;
     private final Context mContext;
     private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+    private final TelecomSystem.SyncRoot mLock;
 
     /*
      * Whether or not the outgoing call intent originated from the default phone application. If
@@ -92,6 +88,7 @@
         mIntent = intent;
         mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
         mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
+        mLock = mCallsManager.getLock();
     }
 
     /**
@@ -105,63 +102,65 @@
             try {
                 Log.startSession("NOCBIR.oR");
                 Trace.beginSection("onReceiveNewOutgoingCallBroadcast");
-                Log.v(this, "onReceive: %s", intent);
+                synchronized (mLock) {
+                    Log.v(this, "onReceive: %s", intent);
 
-                // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is used as the
-                // actual number to call. (If null, no call will be placed.)
-                String resultNumber = getResultData();
-                Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
-                        Log.pii(resultNumber));
+                    // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is
+                    // used as the actual number to call. (If null, no call will be placed.)
+                    String resultNumber = getResultData();
+                    Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
+                            Log.pii(resultNumber));
 
-                boolean endEarly = false;
-                if (resultNumber == null) {
-                    Log.v(this, "Call cancelled (null number), returning...");
-                    endEarly = true;
-                } else if (mPhoneNumberUtilsAdapter.isPotentialLocalEmergencyNumber(
-                        mContext, resultNumber)) {
-                    Log.w(this, "Cannot modify outgoing call to emergency number %s.",
-                            resultNumber);
-                    endEarly = true;
-                }
-
-                if (endEarly) {
-                    if (mCall != null) {
-                        mCall.disconnect(true /* wasViaNewOutgoingCall */);
+                    boolean endEarly = false;
+                    if (resultNumber == null) {
+                        Log.v(this, "Call cancelled (null number), returning...");
+                        endEarly = true;
+                    } else if (mPhoneNumberUtilsAdapter.isPotentialLocalEmergencyNumber(
+                            mContext, resultNumber)) {
+                        Log.w(this, "Cannot modify outgoing call to emergency number %s.",
+                                resultNumber);
+                        endEarly = true;
                     }
-                    return;
+
+                    if (endEarly) {
+                        if (mCall != null) {
+                            mCall.disconnect(true /* wasViaNewOutgoingCall */);
+                        }
+                        return;
+                    }
+
+                    // If this call is already disconnected then we have nothing more to do.
+                    if (mCall.isDisconnected()) {
+                        Log.w(this, "Call has already been disconnected," +
+                                        " ignore the broadcast Call %s", mCall);
+                        return;
+                    }
+
+                    Uri resultHandleUri = Uri.fromParts(
+                            mPhoneNumberUtilsAdapter.isUriNumber(resultNumber) ?
+                                    PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL,
+                            resultNumber, null);
+
+                    Uri originalUri = mIntent.getData();
+
+                    if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
+                        Log.v(this, "Call number unmodified after" +
+                                " new outgoing call intent broadcast.");
+                    } else {
+                        Log.v(this, "Retrieved modified handle after outgoing call intent" +
+                                " broadcast: Original: %s, Modified: %s",
+                                Log.pii(originalUri),
+                                Log.pii(resultHandleUri));
+                    }
+
+                    GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
+                    mCall.setNewOutgoingCallIntentBroadcastIsDone();
+                    mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
+                            mIntent.getBooleanExtra(
+                                    TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false),
+                            mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                                    VideoProfile.STATE_AUDIO_ONLY));
                 }
-
-                // If this call is already disconnected then we have nothing more to do.
-                if (mCall.isDisconnected()) {
-                    Log.w(this,
-                        "Call has already been disconnected, ignore the broadcast Call %s", mCall);
-                    return;
-                }
-
-                Uri resultHandleUri = Uri.fromParts(
-                        mPhoneNumberUtilsAdapter.isUriNumber(resultNumber) ?
-                                PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL,
-                        resultNumber, null);
-
-                Uri originalUri = mIntent.getData();
-
-                if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
-                    Log.v(this, "Call number unmodified after new outgoing call intent broadcast.");
-                } else {
-                    Log.v(this, "Retrieved modified handle after outgoing call intent broadcast: "
-                                    + "Original: %s, Modified: %s",
-                            Log.pii(originalUri),
-                            Log.pii(resultHandleUri));
-                }
-
-                GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
-                mCall.setNewOutgoingCallIntentBroadcastIsDone();
-                mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
-                        mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
-                                false),
-                        mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                                VideoProfile.STATE_AUDIO_ONLY));
-
             } finally {
                 Trace.endSection();
                 Log.endSession();
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 9b65197..9cc61b3 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -28,11 +28,13 @@
  * Utilities dealing with {@link ParcelableCall}.
  */
 public class ParcelableCallUtils {
+    private static final int CALL_STATE_OVERRIDE_NONE = -1;
+
     public static class Converter {
         public ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider,
                 PhoneAccountRegistrar phoneAccountRegistrar) {
             return ParcelableCallUtils.toParcelableCall(
-                    call, includeVideoProvider, phoneAccountRegistrar);
+                    call, includeVideoProvider, phoneAccountRegistrar, false);
         }
     }
 
@@ -45,13 +47,45 @@
      *      method creates a {@link VideoCallImpl} instance on access it is important for the
      *      recipient of the {@link ParcelableCall} to know if the video provider changed.
      * @param phoneAccountRegistrar The {@link PhoneAccountRegistrar}.
+     * @param supportsExternalCalls Indicates whether the call should be parcelled for an
+     *      {@link InCallService} which supports external calls or not.
+     */
+    public static ParcelableCall toParcelableCall(
+            Call call,
+            boolean includeVideoProvider,
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            boolean supportsExternalCalls) {
+        return toParcelableCall(call, includeVideoProvider, phoneAccountRegistrar,
+                supportsExternalCalls, CALL_STATE_OVERRIDE_NONE /* overrideState */);
+    }
+
+    /**
+     * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
+     *
+     * @param call The {@link Call} to parcel.
+     * @param includeVideoProvider {@code true} if the video provider should be parcelled with the
+     *      {@link Call}, {@code false} otherwise.  Since the {@link ParcelableCall#getVideoCall()}
+     *      method creates a {@link VideoCallImpl} instance on access it is important for the
+     *      recipient of the {@link ParcelableCall} to know if the video provider changed.
+     * @param phoneAccountRegistrar The {@link PhoneAccountRegistrar}.
+     * @param supportsExternalCalls Indicates whether the call should be parcelled for an
+     *      {@link InCallService} which supports external calls or not.
+     * @param overrideState When not {@link #CALL_STATE_OVERRIDE_NONE}, use the provided state as an
+     *      override to whatever is defined in the call.
      * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
      */
     public static ParcelableCall toParcelableCall(
             Call call,
             boolean includeVideoProvider,
-            PhoneAccountRegistrar phoneAccountRegistrar) {
-        int state = getParcelableState(call);
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            boolean supportsExternalCalls,
+            int overrideState) {
+        int state;
+        if (overrideState == CALL_STATE_OVERRIDE_NONE) {
+            state = getParcelableState(call, supportsExternalCalls);
+        } else {
+            state = overrideState;
+        }
         int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities());
         int properties = convertConnectionToCallProperties(call.getConnectionProperties());
         if (call.isConference()) {
@@ -63,7 +97,7 @@
         }
 
         // If this is a single-SIM device, the "default SIM" will always be the only SIM.
-        boolean isDefaultSmsAccount =
+        boolean isDefaultSmsAccount = phoneAccountRegistrar != null &&
                 phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
         if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) {
             capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT;
@@ -141,7 +175,7 @@
                 call.getExtras());
     }
 
-    private static int getParcelableState(Call call) {
+    private static int getParcelableState(Call call, boolean supportsExternalCalls) {
         int state = CallState.NEW;
         switch (call.getState()) {
             case CallState.ABORTED:
@@ -157,6 +191,19 @@
             case CallState.DIALING:
                 state = android.telecom.Call.STATE_DIALING;
                 break;
+            case CallState.PULLING:
+                if (supportsExternalCalls) {
+                    // The InCallService supports external calls, so it must handle
+                    // STATE_PULLING_CALL.
+                    state = android.telecom.Call.STATE_PULLING_CALL;
+                } else {
+                    // The InCallService does NOT support external calls, so remap
+                    // STATE_PULLING_CALL to STATE_DIALING.  In essence, pulling a call can be seen
+                    // as a form of dialing, so it is appropriate for InCallServices which do not
+                    // handle external calls.
+                    state = android.telecom.Call.STATE_DIALING;
+                }
+                break;
             case CallState.DISCONNECTING:
                 state = android.telecom.Call.STATE_DISCONNECTING;
                 break;
@@ -267,11 +314,14 @@
         Connection.PROPERTY_GENERIC_CONFERENCE,
         android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE,
 
-        Connection.PROPERTY_SHOW_CALLBACK_NUMBER,
+        Connection.PROPERTY_EMERGENCY_CALLBACK_MODE,
         android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE,
 
         Connection.PROPERTY_IS_EXTERNAL_CALL,
-        android.telecom.Call.Details.PROPERTY_IS_EXTERNAL_CALL
+        android.telecom.Call.Details.PROPERTY_IS_EXTERNAL_CALL,
+
+        Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY,
+        android.telecom.Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY
     };
 
     private static int convertConnectionToCallProperties(int connectionProperties) {
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 19c1a01..41eb9e4 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -59,7 +59,6 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -76,8 +75,12 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
@@ -244,6 +247,46 @@
     }
 
     /**
+     * @return The {@link DefaultPhoneAccountHandle} containing the user-selected default calling
+     * account and group Id for the {@link UserHandle} specified.
+     */
+    private DefaultPhoneAccountHandle getUserSelectedDefaultPhoneAccount(UserHandle userHandle) {
+        if (userHandle == null) {
+            return null;
+        }
+        DefaultPhoneAccountHandle defaultPhoneAccountHandle = mState.defaultOutgoingAccountHandles
+                .get(userHandle);
+        if (defaultPhoneAccountHandle == null) {
+            return null;
+        }
+
+        return defaultPhoneAccountHandle;
+    }
+
+    /**
+     * @return The currently registered PhoneAccount in Telecom that has the same group Id.
+     */
+    private PhoneAccount getPhoneAccountByGroupId(String groupId, ComponentName groupComponentName,
+            UserHandle userHandle, PhoneAccountHandle excludePhoneAccountHandle) {
+        if (groupId == null || groupId.isEmpty() || userHandle == null) {
+            return null;
+        }
+        // Get the PhoneAccount with the same group Id (and same ComponentName) that is not the
+        // newAccount that was just added
+        List<PhoneAccount> accounts = getAllPhoneAccounts(userHandle).stream()
+                .filter(account -> groupId.equals(account.getGroupId()) &&
+                        !account.getAccountHandle().equals(excludePhoneAccountHandle) &&
+                        Objects.equals(account.getAccountHandle().getComponentName(),
+                                groupComponentName))
+                .collect(Collectors.toList());
+        // There should be one or no PhoneAccounts with the same group Id
+        if (accounts.size() > 1) {
+            Log.w(this, "Found multiple PhoneAccounts registered to the same Group Id!");
+        }
+        return accounts.isEmpty() ? null : accounts.get(0);
+    }
+
+    /**
      * Sets the phone account with which to place all calls by default. Set by the user
      * within phone settings.
      */
@@ -277,7 +320,8 @@
             }
 
             mState.defaultOutgoingAccountHandles
-                    .put(userHandle, new DefaultPhoneAccountHandle(userHandle, accountHandle));
+                    .put(userHandle, new DefaultPhoneAccountHandle(userHandle, accountHandle,
+                            account.getGroupId()));
         }
 
         write();
@@ -591,6 +635,8 @@
         }
 
         mState.accounts.add(account);
+        // Set defaults and replace based on the group Id.
+        maybeReplaceOldAccount(account);
         // Reset enabled state to whatever the value was if the account was already registered,
         // or _true_ if this is a SIM-based account.  All SIM-based accounts are always enabled.
         account.setIsEnabled(
@@ -697,6 +743,40 @@
         }
     }
 
+    private void maybeReplaceOldAccount(PhoneAccount newAccount) {
+        UserHandle newAccountUserHandle = newAccount.getAccountHandle().getUserHandle();
+        DefaultPhoneAccountHandle defaultHandle =
+                getUserSelectedDefaultPhoneAccount(newAccountUserHandle);
+        if (defaultHandle == null || defaultHandle.groupId.isEmpty()) {
+            Log.v(this, "maybeReplaceOldAccount: Not replacing PhoneAccount, no group Id or " +
+                    "default.");
+            return;
+        }
+        if (!defaultHandle.groupId.equals(newAccount.getGroupId())) {
+            Log.v(this, "maybeReplaceOldAccount: group Ids are not equal.");
+            return;
+        }
+        if (Objects.equals(newAccount.getAccountHandle().getComponentName(),
+                defaultHandle.phoneAccountHandle.getComponentName())) {
+            // Move default calling account over to new user, since the ComponentNames and Group Ids
+            // are the same.
+            setUserSelectedOutgoingPhoneAccount(newAccount.getAccountHandle(),
+                    newAccountUserHandle);
+        } else {
+            Log.v(this, "maybeReplaceOldAccount: group Ids are equal, but ComponentName is not" +
+                    " the same as the default. Not replacing default PhoneAccount.");
+        }
+        PhoneAccount replacementAccount = getPhoneAccountByGroupId(newAccount.getGroupId(),
+                newAccount.getAccountHandle().getComponentName(), newAccountUserHandle,
+                newAccount.getAccountHandle());
+        if (replacementAccount != null) {
+            // Unregister the old PhoneAccount.
+            Log.v(this, "maybeReplaceOldAccount: Unregistering old PhoneAccount: " +
+                    replacementAccount.getAccountHandle());
+            unregisterPhoneAccount(replacementAccount.getAccountHandle());
+        }
+    }
+
     /**
      * Determines if the connection service specified by a {@link PhoneAccountHandle} requires the
      * {@link Manifest.permission#BIND_TELECOM_CONNECTION_SERVICE} permission.
@@ -904,10 +984,13 @@
 
         public final PhoneAccountHandle phoneAccountHandle;
 
+        public final String groupId;
+
         public DefaultPhoneAccountHandle(UserHandle userHandle,
-                PhoneAccountHandle phoneAccountHandle) {
+                PhoneAccountHandle phoneAccountHandle, String groupId) {
             this.userHandle = userHandle;
             this.phoneAccountHandle = phoneAccountHandle;
+            this.groupId = groupId;
         }
     }
 
@@ -1154,6 +1237,13 @@
             serializer.endTag(null, tagName);
         }
 
+        protected void writeNonNullString(String tagName, String value, XmlSerializer serializer)
+                throws IOException {
+            serializer.startTag(null, tagName);
+            serializer.text(value != null ? value : "");
+            serializer.endTag(null, tagName);
+        }
+
         /**
          * Reads a string array from the XML parser.
          *
@@ -1292,8 +1382,9 @@
                 while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                     if (parser.getName().equals(DEFAULT_OUTGOING)) {
                         if (s.versionNumber < 9) {
-                            // Migration old default phone account handle here by assuming the
-                            // default phone account handle is belong to primary user.
+                            // Migrate old default phone account handle here by assuming the
+                            // default phone account handle belongs to the primary user. Also,
+                            // assume there are no groups.
                             parser.nextTag();
                             PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
                                     .readFromXml(parser, s.versionNumber, context);
@@ -1303,7 +1394,7 @@
                                 UserHandle userHandle = primaryUser.getUserHandle();
                                 DefaultPhoneAccountHandle defaultPhoneAccountHandle
                                         = new DefaultPhoneAccountHandle(userHandle,
-                                        phoneAccountHandle);
+                                        phoneAccountHandle, "" /* groupId */);
                                 s.defaultOutgoingAccountHandles
                                         .put(userHandle, defaultPhoneAccountHandle);
                             }
@@ -1343,6 +1434,7 @@
                 private static final String CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE
                         = "default_outgoing_phone_account_handle";
                 private static final String USER_SERIAL_NUMBER = "user_serial_number";
+                private static final String GROUP_ID = "group_id";
                 private static final String ACCOUNT_HANDLE = "account_handle";
 
                 @Override
@@ -1354,6 +1446,7 @@
                         if (serialNumber != -1) {
                             serializer.startTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
                             writeLong(USER_SERIAL_NUMBER, serialNumber, serializer);
+                            writeNonNullString(GROUP_ID, o.groupId, serializer);
                             serializer.startTag(null, ACCOUNT_HANDLE);
                             sPhoneAccountHandleXml.writeToXml(o.phoneAccountHandle, serializer,
                                     context);
@@ -1371,6 +1464,7 @@
                         int outerDepth = parser.getDepth();
                         PhoneAccountHandle accountHandle = null;
                         String userSerialNumberString = null;
+                        String groupId = "";
                         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                             if (parser.getName().equals(ACCOUNT_HANDLE)) {
                                 parser.nextTag();
@@ -1379,6 +1473,9 @@
                             } else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
                                 parser.next();
                                 userSerialNumberString = parser.getText();
+                            } else if (parser.getName().equals(GROUP_ID)) {
+                                parser.next();
+                                groupId = parser.getText();
                             }
                         }
                         UserHandle userHandle = null;
@@ -1392,8 +1489,9 @@
                                         "Could not parse UserHandle " + userSerialNumberString);
                             }
                         }
-                        if (accountHandle != null && userHandle != null) {
-                            return new DefaultPhoneAccountHandle(userHandle, accountHandle);
+                        if (accountHandle != null && userHandle != null && groupId != null) {
+                            return new DefaultPhoneAccountHandle(userHandle, accountHandle,
+                                    groupId);
                         }
                     }
                     return null;
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
index 41284cb..8e59a64 100644
--- a/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
@@ -24,6 +24,7 @@
  * refactoring.
  */
 public interface PhoneNumberUtilsAdapter {
+    boolean isLocalEmergencyNumber(Context context, String number);
     boolean isPotentialLocalEmergencyNumber(Context context, String number);
     boolean isUriNumber(String number);
     String getNumberFromIntent(Intent intent, Context context);
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
index 640d814..fa316a5 100644
--- a/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
@@ -22,6 +22,11 @@
 
 public class PhoneNumberUtilsAdapterImpl implements PhoneNumberUtilsAdapter {
     @Override
+    public boolean isLocalEmergencyNumber(Context context, String number) {
+            return PhoneNumberUtils.isLocalEmergencyNumber(context, number);
+    }
+
+    @Override
     public boolean isPotentialLocalEmergencyNumber(Context context, String number) {
         return PhoneNumberUtils.isPotentialLocalEmergencyNumber(context, number);
     }
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 413f06c..0781ca2 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -43,6 +43,9 @@
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (call.isExternalCall()) {
+            return;
+        }
         updateStates(call);
     }
 
@@ -83,8 +86,8 @@
         int callState = TelephonyManager.CALL_STATE_IDLE;
         if (mCallsManager.hasRingingCall()) {
             callState = TelephonyManager.CALL_STATE_RINGING;
-        } else if (mCallsManager.getFirstCallWithState(CallState.DIALING, CallState.ACTIVE,
-                    CallState.ON_HOLD) != null) {
+        } else if (mCallsManager.getFirstCallWithState(CallState.DIALING, CallState.PULLING,
+                CallState.ACTIVE, CallState.ON_HOLD) != null) {
             callState = TelephonyManager.CALL_STATE_OFFHOOK;
         }
         sendPhoneStateChangedBroadcast(call, callState);
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
index dd336c4..e53b1d5 100644
--- a/src/com/android/server/telecom/ProximitySensorManager.java
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -38,6 +38,9 @@
 
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         if (mCallsManager.getCalls().isEmpty()) {
             Log.i(this, "All calls removed, resetting proximity sensor to default state");
             turnOff(true);
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 29ea53c..7609b08 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -30,6 +30,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.telecom.Connection;
 import android.telecom.Response;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionManager;
@@ -131,8 +132,7 @@
     public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
         if (rejectWithMessage
                 && call.getHandle() != null
-                && !call.can(
-                        android.telecom.Call.Details.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION)) {
+                && !call.can(Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION)) {
             int subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(
                     call.getTargetPhoneAccount());
             rejectCallWithMessage(call.getContext(), call.getHandle().getSchemeSpecificPart(),
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 77b1590..262f437 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -94,30 +94,32 @@
         mInCallController = inCallController;
     }
 
-    public void startRinging(Call foregroundCall) {
+    public boolean startRinging(Call foregroundCall) {
+        AudioManager audioManager =
+                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        boolean isRingerAudible = audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
+
         if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
-            return;
+            return false;
         }
 
         if (foregroundCall == null) {
             Log.wtf(this, "startRinging called with null foreground call.");
-            return;
+            return false;
         }
 
         if (mInCallController.doesConnectedDialerSupportRinging()) {
             Log.event(foregroundCall, Log.Events.SKIP_RINGING);
-            return;
+            return isRingerAudible;
         }
 
         stopCallWaiting();
 
         if (!shouldRingForContact(foregroundCall.getContactUri())) {
-            return;
+            return false;
         }
 
-        AudioManager audioManager =
-                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
-        if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+        if (isRingerAudible) {
             mRingingCall = foregroundCall;
             Log.event(foregroundCall, Log.Events.START_RINGER);
             // Because we wait until a contact info query to complete before processing a
@@ -126,7 +128,7 @@
             // request the custom ringtone from the call and expect it to be current.
             mRingtonePlayer.play(mRingtoneFactory, foregroundCall);
         } else {
-            Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
+            Log.i(this, "startRingingOrCallWaiting, skipping because volume is 0");
         }
 
         if (shouldVibrate(mContext) && !mIsVibrating) {
@@ -134,6 +136,8 @@
                     VIBRATION_ATTRIBUTES);
             mIsVibrating = true;
         }
+
+        return isRingerAudible;
     }
 
     public void startCallWaiting(Call call) {
diff --git a/src/com/android/server/telecom/Runnable.java b/src/com/android/server/telecom/Runnable.java
index 41415fd..c7ace72 100644
--- a/src/com/android/server/telecom/Runnable.java
+++ b/src/com/android/server/telecom/Runnable.java
@@ -24,7 +24,7 @@
 
     private Session mSubsession;
     private final String mSubsessionName;
-    private final Object mLock = new Object();
+    private final Object mLock;
     private final java.lang.Runnable mRunnable = new java.lang.Runnable() {
             @Override
             public void run() {
@@ -42,7 +42,18 @@
             }
         };
 
-    public Runnable(String subsessionName) {
+    /**
+     * Creates a new Telecom Runnable that incorporates Session Logging into it. Useful for carrying
+     * Logging Sessions through different threads as well as through handlers.
+     * @param subsessionName The name that will be used in the Logs to mark this Session
+     * @param lock The synchronization lock that will be used to lock loggedRun().
+     */
+    public Runnable(String subsessionName, Object lock) {
+        if (lock == null) {
+            mLock = new Object();
+        } else {
+            mLock = lock;
+        }
         mSubsessionName = subsessionName;
     }
 
@@ -78,7 +89,8 @@
     }
 
     /**
-     * The method that will be run in the handler/thread.
+     * The method that will be run in the handler/thread. This method will be called with mLock
+     * held.
      */
     abstract public void loggedRun();
 
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 18d6581..9a0f7b4 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -336,22 +336,28 @@
      */
     private void setBinder(IBinder binder) {
         if (mBinder != binder) {
-            mBinder = binder;
-
-            setServiceInterface(binder);
-
             if (binder == null) {
+                removeServiceInterface();
+                mBinder = null;
                 for (Listener l : mListeners) {
                     l.onUnbind(this);
                 }
+            } else {
+                mBinder = binder;
+                setServiceInterface(binder);
             }
         }
     }
 
     /**
-     * Sets the service interface after the service is bound or unbound.
+     * Sets the service interface after the service is bound.
      *
-     * @param binder The actual bound service implementation.
+     * @param binder The new binder interface that is being set.
      */
     protected abstract void setServiceInterface(IBinder binder);
+
+    /**
+     * Removes the service interface before the service is unbound.
+     */
+    protected abstract void removeServiceInterface();
 }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 577d93a..c93a752 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -39,9 +39,9 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.telecom.DefaultDialerManager;
-import android.telecom.ParcelableCallAnalytics;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomAnalytics;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.SubscriptionManager;
@@ -56,8 +56,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -1145,11 +1143,11 @@
         }
 
         @Override
-        public List<ParcelableCallAnalytics> dumpCallAnalytics() {
+        public TelecomAnalytics dumpCallAnalytics() {
             try {
                 Log.startSession("TSI.dCA");
                 enforcePermission(DUMP);
-                return Arrays.asList(Analytics.dumpToParcelableAnalytics());
+                return Analytics.dumpToParcelableAnalytics();
             } finally {
                 Log.endSession();
             }
@@ -1173,6 +1171,11 @@
                 return;
             }
 
+            if (args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(args[0])) {
+                Analytics.dumpToEncodedProto(writer, args);
+                return;
+            }
+
             final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
             if (mCallsManager != null) {
                 pw.println("CallsManager: ");
@@ -1293,6 +1296,7 @@
             call = mCallsManager.getFirstCallWithState(
                     CallState.ACTIVE,
                     CallState.DIALING,
+                    CallState.PULLING,
                     CallState.RINGING,
                     CallState.ON_HOLD);
         }
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7134fe5..d665a82 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -178,7 +178,9 @@
             BluetoothPhoneServiceImplFactory
                     bluetoothPhoneServiceImplFactory,
             Timeouts.Adapter timeoutsAdapter,
-            AsyncRingtonePlayer asyncRingtonePlayer) {
+            AsyncRingtonePlayer asyncRingtonePlayer,
+            PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
+            InterruptionFilterProxy interruptionFilterProxy) {
         mContext = context.getApplicationContext();
         Log.setContext(mContext);
         Log.initMd5Sum();
@@ -220,7 +222,9 @@
                 systemStateProvider,
                 defaultDialerAdapter,
                 timeoutsAdapter,
-                asyncRingtonePlayer);
+                asyncRingtonePlayer,
+                phoneNumberUtilsAdapter,
+                interruptionFilterProxy);
 
         mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
         mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 69adaf9..50b8901 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -22,9 +22,15 @@
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
 /**
  * Utilities to deal with the system telephony services. The system telephony services are treated
  * differently from 3rd party services in some situations (emergency calls, audio focus, etc...).
@@ -67,4 +73,55 @@
         return handle != null && PhoneNumberUtils.isLocalEmergencyNumber(
                 context, handle.getSchemeSpecificPart());
     }
+
+    public static void sortSimPhoneAccounts(Context context, List<PhoneAccount> accounts) {
+        final TelephonyManager telephonyManager = TelephonyManager.from(context);
+
+        // Sort the accounts according to how we want to display them.
+        Collections.sort(accounts, new Comparator<PhoneAccount>() {
+            @Override
+            public int compare(PhoneAccount account1, PhoneAccount account2) {
+                int retval = 0;
+
+                // SIM accounts go first
+                boolean isSim1 = account1.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+                boolean isSim2 = account2.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+                if (isSim1 != isSim2) {
+                    retval = isSim1 ? -1 : 1;
+                }
+
+                int subId1 = telephonyManager.getSubIdForPhoneAccount(account1);
+                int subId2 = telephonyManager.getSubIdForPhoneAccount(account2);
+                if (subId1 != SubscriptionManager.INVALID_SUBSCRIPTION_ID &&
+                        subId2 != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                    retval = (SubscriptionManager.getSlotId(subId1) <
+                            SubscriptionManager.getSlotId(subId2)) ? -1 : 1;
+                }
+
+                // Then order by package
+                if (retval == 0) {
+                    String pkg1 = account1.getAccountHandle().getComponentName().getPackageName();
+                    String pkg2 = account2.getAccountHandle().getComponentName().getPackageName();
+                    retval = pkg1.compareTo(pkg2);
+                }
+
+                // Finally, order by label
+                if (retval == 0) {
+                    String label1 = nullToEmpty(account1.getLabel().toString());
+                    String label2 = nullToEmpty(account2.getLabel().toString());
+                    retval = label1.compareTo(label2);
+                }
+
+                // Then by hashcode
+                if (retval == 0) {
+                    retval = account1.hashCode() - account2.hashCode();
+                }
+                return retval;
+            }
+        });
+    }
+
+    private static String nullToEmpty(String str) {
+        return str == null ? "" : str;
+    }
 }
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 08d7410..7be59c3 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -32,6 +32,10 @@
         public long getCallScreeningTimeoutMillis(ContentResolver cr) {
             return Timeouts.getCallScreeningTimeoutMillis(cr);
         }
+
+        public long getCallRemoveUnbindInCallServicesDelay(ContentResolver cr) {
+            return Timeouts.getCallRemoveUnbindInCallServicesDelay(cr);
+        }
     }
 
     /** A prefix to use for all keys so to not clobber the global namespace. */
@@ -53,22 +57,13 @@
     }
 
     /**
-     * Returns the longest period, in milliseconds, to wait for the query for direct-to-voicemail
-     * to complete. If the query goes beyond this timeout, the incoming call screen is shown to the
-     * user.
-     */
-    public static long getDirectToVoicemailMillis(ContentResolver contentResolver) {
-        return get(contentResolver, "direct_to_voicemail_ms", 500L);
-    }
-
-    /**
      * Returns the amount of time to wait before disconnecting a call that was canceled via
      * NEW_OUTGOING_CALL broadcast. This timeout allows apps which repost the call using a gateway
      * to reuse the existing call, preventing the call from causing a start->end->start jank in the
      * in-call UI.
      */
     public static long getNewOutgoingCallCancelMillis(ContentResolver contentResolver) {
-        return get(contentResolver, "new_outgoing_call_cancel_ms", 400L);
+        return get(contentResolver, "new_outgoing_call_cancel_ms", 100000L);
     }
 
     /**
@@ -141,11 +136,4 @@
     public static long getCallScreeningTimeoutMillis(ContentResolver contentResolver) {
         return get(contentResolver, "call_screening_timeout", 5000L /* 5 seconds */);
     }
-
-    /**
-     * Returns the amount of time to wait for the block checker to allow or disallow a call.
-     */
-    public static long getBlockCheckTimeoutMillis(ContentResolver contentResolver) {
-        return get(contentResolver, "block_check_timeout_millis", 500L);
-    }
 }
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index 9d17139..3722b59 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -29,7 +29,6 @@
 import com.android.internal.telecom.IVideoProvider;
 
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -130,6 +129,31 @@
                 Log.startSession("VPP.rSMR");
                 synchronized (mLock) {
                     logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile);
+                    Log.event(mCall, Log.Events.RECEIVE_VIDEO_REQUEST,
+                            VideoProfile.videoStateToString(videoProfile.getVideoState()));
+
+                    mCall.getAnalytics().addVideoEvent(
+                            Analytics.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST,
+                            videoProfile.getVideoState());
+
+                    if (!mCall.isVideoCallingSupported() &&
+                            VideoProfile.isVideo(videoProfile.getVideoState())) {
+                        // If video calling is not supported by the phone account, and we receive
+                        // a request to upgrade to video, automatically reject it without informing
+                        // the InCallService.
+
+                        Log.event(mCall, Log.Events.SEND_VIDEO_RESPONSE, "video not supported");
+                        VideoProfile responseProfile = new VideoProfile(
+                                VideoProfile.STATE_AUDIO_ONLY);
+                        try {
+                            mConectionServiceVideoProvider.sendSessionModifyResponse(
+                                    responseProfile);
+                        } catch (RemoteException e) {
+                        }
+
+                        // Don't want to inform listeners of the request as we've just rejected it.
+                        return;
+                    }
 
                     // Inform other Telecom components of the session modification request.
                     for (Listener listener : mListeners) {
@@ -154,10 +178,19 @@
         @Override
         public void receiveSessionModifyResponse(int status, VideoProfile requestProfile,
                 VideoProfile responseProfile) {
+            logFromVideoProvider("receiveSessionModifyResponse: status=" + status +
+                    " requestProfile=" + requestProfile + " responseProfile=" + responseProfile);
+            String eventMessage = "Status Code : " + status + " Video State: " +
+                    (responseProfile != null ? responseProfile.getVideoState() : "null");
+            Log.event(mCall, Log.Events.RECEIVE_VIDEO_RESPONSE, eventMessage);
             synchronized (mLock) {
-                logFromVideoProvider("receiveSessionModifyResponse: status=" + status +
-                        " requestProfile=" + requestProfile + " responseProfile=" +
-                        responseProfile);
+                if (status == Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+                    mCall.getAnalytics().addVideoEvent(
+                            Analytics.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE,
+                            responseProfile == null ?
+                                    VideoProfile.STATE_AUDIO_ONLY :
+                                    responseProfile.getVideoState());
+                }
                 VideoProviderProxy.this.receiveSessionModifyResponse(status, requestProfile,
                         responseProfile);
             }
@@ -172,7 +205,8 @@
         @Override
         public void handleCallSessionEvent(int event) {
             synchronized (mLock) {
-                logFromVideoProvider("handleCallSessionEvent: " + event);
+                logFromVideoProvider("handleCallSessionEvent: " +
+                        Connection.VideoProvider.sessionEventToString(event));
                 VideoProviderProxy.this.handleCallSessionEvent(event);
             }
         }
@@ -337,6 +371,11 @@
     public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
         synchronized (mLock) {
             logFromInCall("sendSessionModifyRequest: from=" + fromProfile + " to=" + toProfile);
+            Log.event(mCall, Log.Events.SEND_VIDEO_REQUEST,
+                    VideoProfile.videoStateToString(toProfile.getVideoState()));
+            mCall.getAnalytics().addVideoEvent(
+                    Analytics.SEND_LOCAL_SESSION_MODIFY_REQUEST,
+                    toProfile.getVideoState());
             try {
                 mConectionServiceVideoProvider.sendSessionModifyRequest(fromProfile, toProfile);
             } catch (RemoteException e) {
@@ -354,6 +393,11 @@
     public void onSendSessionModifyResponse(VideoProfile responseProfile) {
         synchronized (mLock) {
             logFromInCall("sendSessionModifyResponse: " + responseProfile);
+            Log.event(mCall, Log.Events.SEND_VIDEO_RESPONSE,
+                    VideoProfile.videoStateToString(responseProfile.getVideoState()));
+            mCall.getAnalytics().addVideoEvent(
+                    Analytics.SEND_LOCAL_SESSION_MODIFY_RESPONSE,
+                    responseProfile.getVideoState());
             try {
                 mConectionServiceVideoProvider.sendSessionModifyResponse(responseProfile);
             } catch (RemoteException e) {
@@ -434,7 +478,7 @@
      * @param toLog The message to log.
      */
     private void logFromInCall(String toLog) {
-        Log.v(this, "IC->VP: " + toLog);
+        Log.i(this, "IC->VP (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
     }
 
     /**
@@ -444,6 +488,6 @@
      * @param toLog The message to log.
      */
     private void logFromVideoProvider(String toLog) {
-        Log.v(this, "VP->IC: " + toLog);
+        Log.i(this, "VP->IC (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
     }
 }
diff --git a/src/com/android/server/telecom/callfiltering/AsyncBlockCheckFilter.java b/src/com/android/server/telecom/callfiltering/AsyncBlockCheckFilter.java
index 8872297..51fc390 100644
--- a/src/com/android/server/telecom/callfiltering/AsyncBlockCheckFilter.java
+++ b/src/com/android/server/telecom/callfiltering/AsyncBlockCheckFilter.java
@@ -33,7 +33,8 @@
     private final Context mContext;
     private final BlockCheckerAdapter mBlockCheckerAdapter;
     private Call mIncomingCall;
-    private Session mLogSubsession;
+    private Session mBackgroundTaskSubsession;
+    private Session mPostExecuteSubsession;
     private CallFilterResultCallback mCallback;
 
     public AsyncBlockCheckFilter(Context context, BlockCheckerAdapter blockCheckerAdapter) {
@@ -52,13 +53,14 @@
 
     @Override
     protected void onPreExecute() {
-        mLogSubsession = Log.createSubsession();
+        mBackgroundTaskSubsession = Log.createSubsession();
+        mPostExecuteSubsession = Log.createSubsession();
     }
 
     @Override
     protected Boolean doInBackground(String... params) {
         try {
-            Log.continueSession(mLogSubsession, "ABCF.dIB");
+            Log.continueSession(mBackgroundTaskSubsession, "ABCF.dIB");
             Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_INITIATED);
             return mBlockCheckerAdapter.isBlocked(mContext, params[0]);
         } finally {
@@ -68,23 +70,28 @@
 
     @Override
     protected void onPostExecute(Boolean isBlocked) {
-        CallFilteringResult result;
-        if (isBlocked) {
-            result = new CallFilteringResult(
-                    false, // shouldAllowCall
-                    true, //shouldReject
-                    false, //shouldAddToCallLog
-                    false // shouldShowNotification
-            );
-        } else {
-            result = new CallFilteringResult(
-                    true, // shouldAllowCall
-                    false, // shouldReject
-                    true, // shouldAddToCallLog
-                    true // shouldShowNotification
-            );
+        Log.continueSession(mPostExecuteSubsession, "ABCF.oPE");
+        try {
+            CallFilteringResult result;
+            if (isBlocked) {
+                result = new CallFilteringResult(
+                        false, // shouldAllowCall
+                        true, //shouldReject
+                        false, //shouldAddToCallLog
+                        false // shouldShowNotification
+                );
+            } else {
+                result = new CallFilteringResult(
+                        true, // shouldAllowCall
+                        false, // shouldReject
+                        true, // shouldAddToCallLog
+                        true // shouldShowNotification
+                );
+            }
+            Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_FINISHED, result);
+            mCallback.onCallFilteringComplete(mIncomingCall, result);
+        } finally {
+            Log.endSession();
         }
-        Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_FINISHED, result);
-        mCallback.onCallFilteringComplete(mIncomingCall, result);
     }
 }
diff --git a/src/com/android/server/telecom/callfiltering/DirectToVoicemailCallFilter.java b/src/com/android/server/telecom/callfiltering/DirectToVoicemailCallFilter.java
index 1363d62..1aaae46 100644
--- a/src/com/android/server/telecom/callfiltering/DirectToVoicemailCallFilter.java
+++ b/src/com/android/server/telecom/callfiltering/DirectToVoicemailCallFilter.java
@@ -23,6 +23,8 @@
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.Log;
 
+import java.util.Objects;
+
 public class DirectToVoicemailCallFilter implements IncomingCallFilter.CallFilter {
     private final CallerInfoLookupHelper mCallerInfoLookupHelper;
 
@@ -34,13 +36,14 @@
     public void startFilterLookup(final Call call, CallFilterResultCallback callback) {
         Log.event(call, Log.Events.DIRECT_TO_VM_INITIATED);
         final Uri callHandle = call.getHandle();
+
         mCallerInfoLookupHelper.startLookup(callHandle,
                 new CallerInfoLookupHelper.OnQueryCompleteListener() {
                     @Override
                     public void onCallerInfoQueryComplete(Uri handle, CallerInfo info) {
                         CallFilteringResult result;
-                        if (callHandle.equals(handle)) {
-                            if (info.shouldSendToVoicemail) {
+                        if (Objects.equals(callHandle, handle)) {
+                            if (info != null && info.shouldSendToVoicemail) {
                                 result = new CallFilteringResult(
                                         false, // shouldAllowCall
                                         true, // shouldReject
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilter.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilter.java
index 4685ec0..6e0c684 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilter.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilter.java
@@ -70,17 +70,15 @@
         for (CallFilter filter : mFilters) {
             filter.startFilterLookup(mCall, this);
         }
-        mHandler.postDelayed(new Runnable("ICF.pFTO") { // performFiltering time-out
+        // synchronized to prevent a race on mResult and to enter into Telecom.
+        mHandler.postDelayed(new Runnable("ICF.pFTO", mTelecomLock) { // performFiltering time-out
             @Override
             public void loggedRun() {
-                // synchronized to prevent a race on mResult and to enter into Telecom.
-                synchronized (mTelecomLock) {
-                    if (mIsPending) {
-                        Log.i(IncomingCallFilter.this, "Call filtering has timed out.");
-                        Log.event(mCall, Log.Events.FILTERING_TIMED_OUT);
-                        mListener.onCallFilteringComplete(mCall, mResult);
-                        mIsPending = false;
-                    }
+                if (mIsPending) {
+                    Log.i(IncomingCallFilter.this, "Call filtering has timed out.");
+                    Log.event(mCall, Log.Events.FILTERING_TIMED_OUT);
+                    mListener.onCallFilteringComplete(mCall, mResult);
+                    mIsPending = false;
                 }
             }
         }.prepare(), mTimeoutsAdapter.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
@@ -91,16 +89,14 @@
             mNumPendingFilters--;
             mResult = result.combine(mResult);
             if (mNumPendingFilters == 0) {
-                mHandler.post(new Runnable("ICF.oCFC") {
+                // synchronized on mTelecomLock to enter into Telecom.
+                mHandler.post(new Runnable("ICF.oCFC", mTelecomLock) {
                     @Override
                     public void loggedRun() {
-                        // synchronized to enter into Telecom.
-                        synchronized (mTelecomLock) {
-                            if (mIsPending) {
-                                Log.event(mCall, Log.Events.FILTERING_COMPLETED, mResult);
-                                mListener.onCallFilteringComplete(mCall, mResult);
-                                mIsPending = false;
-                            }
+                        if (mIsPending) {
+                            Log.event(mCall, Log.Events.FILTERING_COMPLETED, mResult);
+                            mListener.onCallFilteringComplete(mCall, mResult);
+                            mIsPending = false;
                         }
                     }
                 }.prepare());
diff --git a/src/com/android/server/telecom/components/ChangeDefaultDialerDialog.java b/src/com/android/server/telecom/components/ChangeDefaultDialerDialog.java
index 5fd7904..107389b 100644
--- a/src/com/android/server/telecom/components/ChangeDefaultDialerDialog.java
+++ b/src/com/android/server/telecom/components/ChangeDefaultDialerDialog.java
@@ -21,11 +21,18 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Color;
+import android.graphics.Typeface;
 import android.os.Bundle;
 import android.telecom.DefaultDialerManager;
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 import android.util.Log;
 
 import com.android.internal.app.AlertActivity;
@@ -57,7 +64,7 @@
         }
 
         // Show dialog to require user confirmation.
-         buildDialog(oldPackage, mNewPackage);
+         buildDialog(mNewPackage);
     }
 
     @Override
@@ -81,36 +88,26 @@
         }
 
         if (!DefaultDialerManager.getInstalledDialerApplications(this).contains(newPackage)) {
-            Log.w(TAG, "Provided package name does not correspond to an installed Dialer "
+            Log.w(TAG, "Provided package name does not correspond to an installed Phone "
                     + "application.");
             return false;
         }
 
         if (!TextUtils.isEmpty(oldPackage) && TextUtils.equals(oldPackage, newPackage)) {
-            Log.w(TAG, "Provided package name is already the current default Dialer application.");
+            Log.w(TAG, "Provided package name is already the current default Phone application.");
             return false;
         }
         return true;
     }
 
-    private boolean buildDialog(String oldPackage, String newPackage) {
+    private boolean buildDialog(String newPackage) {
         final PackageManager pm = getPackageManager();
-        final String newPackageLabel =
-                getApplicationLabelForPackageName(pm, newPackage);
+        final String newPackageLabel = getApplicationLabelForPackageName(pm, newPackage);
         final AlertController.AlertParams p = mAlertParams;
-        p.mTitle = getString(R.string.change_default_dialer_dialog_title);
-        if (!TextUtils.isEmpty(oldPackage)) {
-            String oldPackageLabel =
-                    getApplicationLabelForPackageName(pm, oldPackage);
-            p.mMessage = getString(R.string.change_default_dialer_with_previous_app_set_text,
-                    newPackageLabel,
-                    oldPackageLabel);
-        } else {
-            p.mMessage = getString(R.string.change_default_dialer_no_previous_app_set_text,
-                    newPackageLabel);
-        }
-        p.mPositiveButtonText = getString(android.R.string.yes);
-        p.mNegativeButtonText = getString(android.R.string.no);
+        p.mTitle = getString(R.string.change_default_dialer_dialog_title, newPackageLabel);
+        p.mMessage = getString(R.string.change_default_dialer_warning_message, newPackageLabel);
+        p.mPositiveButtonText = getString(R.string.change_default_dialer_dialog_affirmative);
+        p.mNegativeButtonText = getString(R.string.change_default_dialer_dialog_negative);
         p.mPositiveButtonListener = this;
         p.mNegativeButtonListener = this;
         setupAlert();
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index bef8453..c7fd9e0 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.components;
 
+import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.Service;
 import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
@@ -35,7 +37,10 @@
 import com.android.server.telecom.HeadsetMediaButtonFactory;
 import com.android.server.telecom.InCallWakeLockControllerFactory;
 import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.InterruptionFilterProxy;
 import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
 import com.android.server.telecom.ProximitySensorManagerFactory;
 import com.android.server.telecom.InCallWakeLockController;
 import com.android.server.telecom.Log;
@@ -71,6 +76,9 @@
      */
     static void initializeTelecomSystem(Context context) {
         if (TelecomSystem.getInstance() == null) {
+            final NotificationManager notificationManager =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
             TelecomSystem.setInstance(
                     new TelecomSystem(
                             context,
@@ -148,7 +156,19 @@
                                 }
                             },
                             new Timeouts.Adapter(),
-                            new AsyncRingtonePlayer()
+                            new AsyncRingtonePlayer(),
+                            new PhoneNumberUtilsAdapterImpl(),
+                            new InterruptionFilterProxy() {
+                                @Override
+                                public void setInterruptionFilter(int interruptionFilter) {
+                                    notificationManager.setInterruptionFilter(interruptionFilter);
+                                }
+
+                                @Override
+                                public int getCurrentInterruptionFilter() {
+                                    return notificationManager.getCurrentInterruptionFilter();
+                                }
+                            }
                     ));
         }
         if (BluetoothAdapter.getDefaultAdapter() != null) {
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 21af1aa..9530f63 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -31,6 +31,7 @@
 import com.android.server.telecom.Log;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.R;
 import com.android.server.telecom.Runnable;
 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
@@ -167,7 +168,7 @@
     }
 
     private void markMissedCallsAsRead(final UserHandle userHandle) {
-        AsyncTask.execute(new Runnable("MCNI.mMCAR") {
+        AsyncTask.execute(new Runnable("MCNI.mMCAR", null /*lock*/) {
             @Override
             public void loggedRun() {
                 // Clear the list of new missed calls from the call log.
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
index b30e372..0150dbe 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -165,6 +165,9 @@
             capabilities |= CAPABILITY_RESPOND_VIA_TEXT;
             setConnectionCapabilities(capabilities);
 
+            if (isIncoming) {
+                putExtra(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
+            }
             LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
                     mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS));
             final IntentFilter filter =
diff --git a/tests/Android.mk b/tests/Android.mk
index e639b55..8a8113b 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -21,11 +21,17 @@
         android-ex-camera2 \
         android-support-v4 \
         guava \
-        mockito-target
+        mockito-target \
+        platform-test-annotations
 
 LOCAL_SRC_FILES := \
         $(call all-java-files-under, src) \
-        $(call all-java-files-under, ../src)
+        $(call all-java-files-under, ../src) \
+        $(call all-proto-files-under, ../proto)
+
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
+LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/../proto/
+LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors
 
 LOCAL_RESOURCE_DIR := \
     $(LOCAL_PATH)/res \
@@ -46,6 +52,9 @@
 
 LOCAL_MODULE_TAGS := tests
 
+LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.server.telecom.*
+LOCAL_JACK_COVERAGE_EXCLUDE_FILTER := com.android.server.telecom.tests.*
+
 include frameworks/base/packages/SettingsLib/common.mk
 
 include $(BUILD_PACKAGE)
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
new file mode 100644
index 0000000..81d4aa5
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -0,0 +1,343 @@
+/*
+ * 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.server.telecom.tests;
+
+import android.content.Context;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.InCallService;
+import android.telecom.ParcelableCallAnalytics;
+import android.telecom.TelecomAnalytics;
+import android.telecom.TelecomManager;
+import android.telecom.VideoCallImpl;
+import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Base64;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.Log;
+import com.android.server.telecom.TelecomLogClass;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+public class AnalyticsTests extends TelecomSystemTest {
+    @MediumTest
+    public void testAnalyticsSingleCall() throws Exception {
+        IdPair testCall = startAndMakeActiveIncomingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+
+        assertTrue(analyticsMap.containsKey(testCall.mCallId));
+
+        Analytics.CallInfoImpl callAnalytics = analyticsMap.get(testCall.mCallId);
+        assertTrue(callAnalytics.startTime > 0);
+        assertEquals(0, callAnalytics.endTime);
+        assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics.callDirection);
+        assertFalse(callAnalytics.isInterrupted);
+        assertNull(callAnalytics.callTerminationReason);
+        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+                callAnalytics.connectionService);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+
+        analyticsMap = Analytics.cloneData();
+        callAnalytics = analyticsMap.get(testCall.mCallId);
+        assertTrue(callAnalytics.endTime > 0);
+        assertNotNull(callAnalytics.callTerminationReason);
+        assertEquals(DisconnectCause.ERROR, callAnalytics.callTerminationReason.getCode());
+
+        StringWriter sr = new StringWriter();
+        IndentingPrintWriter ip = new IndentingPrintWriter(sr, "    ");
+        Analytics.dump(ip);
+        String dumpResult = sr.toString();
+        String[] expectedFields = {"startTime", "endTime", "direction", "isAdditionalCall",
+                "isInterrupted", "callTechnologies", "callTerminationReason", "connectionService"};
+        for (String field : expectedFields) {
+            assertTrue(dumpResult.contains(field));
+        }
+    }
+
+    @MediumTest
+    public void testAnalyticsDumping() throws Exception {
+        Analytics.reset();
+        IdPair testCall = startAndMakeActiveIncomingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+        Analytics.CallInfoImpl expectedAnalytics = Analytics.cloneData().get(testCall.mCallId);
+
+        TelecomManager tm = (TelecomManager) mSpyContext.getSystemService(Context.TELECOM_SERVICE);
+        List<ParcelableCallAnalytics> analyticsList = tm.dumpAnalytics().getCallAnalytics();
+
+        assertEquals(1, analyticsList.size());
+        ParcelableCallAnalytics pCA = analyticsList.get(0);
+
+        assertTrue(Math.abs(expectedAnalytics.startTime - pCA.getStartTimeMillis()) <
+                ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+        assertEquals(0, pCA.getStartTimeMillis() % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+        assertTrue(Math.abs((expectedAnalytics.endTime - expectedAnalytics.startTime) -
+                pCA.getCallDurationMillis()) < ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+        assertEquals(0, pCA.getCallDurationMillis() % ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+
+        assertEquals(expectedAnalytics.callDirection, pCA.getCallType());
+        assertEquals(expectedAnalytics.isAdditionalCall, pCA.isAdditionalCall());
+        assertEquals(expectedAnalytics.isInterrupted, pCA.isInterrupted());
+        assertEquals(expectedAnalytics.callTechnologies, pCA.getCallTechnologies());
+        assertEquals(expectedAnalytics.callTerminationReason.getCode(),
+                pCA.getCallTerminationCode());
+        assertEquals(expectedAnalytics.connectionService, pCA.getConnectionService());
+        List<ParcelableCallAnalytics.AnalyticsEvent> analyticsEvents = pCA.analyticsEvents();
+        Set<Integer> capturedEvents = new HashSet<>();
+        for (ParcelableCallAnalytics.AnalyticsEvent e : analyticsEvents) {
+            capturedEvents.add(e.getEventName());
+            assertIsRoundedToOneSigFig(e.getTimeSinceLastEvent());
+        }
+        assertTrue(capturedEvents.contains(ParcelableCallAnalytics.AnalyticsEvent.SET_ACTIVE));
+        assertTrue(capturedEvents.contains(
+                ParcelableCallAnalytics.AnalyticsEvent.FILTERING_INITIATED));
+    }
+
+    @MediumTest
+    public void testAnalyticsTwoCalls() throws Exception {
+        IdPair testCall1 = startAndMakeActiveIncomingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+        IdPair testCall2 = startAndMakeActiveOutgoingCall(
+                "650-555-1213",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+        assertTrue(analyticsMap.containsKey(testCall1.mCallId));
+        assertTrue(analyticsMap.containsKey(testCall2.mCallId));
+
+        Analytics.CallInfoImpl callAnalytics1 = analyticsMap.get(testCall1.mCallId);
+        Analytics.CallInfoImpl callAnalytics2 = analyticsMap.get(testCall2.mCallId);
+        assertTrue(callAnalytics1.startTime > 0);
+        assertTrue(callAnalytics2.startTime > 0);
+        assertEquals(0, callAnalytics1.endTime);
+        assertEquals(0, callAnalytics2.endTime);
+
+        assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics1.callDirection);
+        assertEquals(Analytics.OUTGOING_DIRECTION, callAnalytics2.callDirection);
+
+        assertTrue(callAnalytics1.isInterrupted);
+        assertTrue(callAnalytics2.isAdditionalCall);
+
+        assertNull(callAnalytics1.callTerminationReason);
+        assertNull(callAnalytics2.callTerminationReason);
+
+        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+                callAnalytics1.connectionService);
+        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+                callAnalytics1.connectionService);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall2.mConnectionId, DisconnectCause.REMOTE);
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall1.mConnectionId, DisconnectCause.ERROR);
+
+        analyticsMap = Analytics.cloneData();
+        callAnalytics1 = analyticsMap.get(testCall1.mCallId);
+        callAnalytics2 = analyticsMap.get(testCall2.mCallId);
+        assertTrue(callAnalytics1.endTime > 0);
+        assertTrue(callAnalytics2.endTime > 0);
+        assertNotNull(callAnalytics1.callTerminationReason);
+        assertNotNull(callAnalytics2.callTerminationReason);
+        assertEquals(DisconnectCause.ERROR, callAnalytics1.callTerminationReason.getCode());
+        assertEquals(DisconnectCause.REMOTE, callAnalytics2.callTerminationReason.getCode());
+    }
+
+    @MediumTest
+    public void testAnalyticsVideo() throws Exception {
+        Analytics.reset();
+        IdPair callIds = startAndMakeActiveOutgoingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        CountDownLatch counter = new CountDownLatch(1);
+        InCallService.VideoCall.Callback callback = mock(InCallService.VideoCall.Callback.class);
+
+        doAnswer(invocation -> {
+            counter.countDown();
+            return null;
+        }).when(callback)
+                .onSessionModifyResponseReceived(anyInt(), any(VideoProfile.class),
+                        any(VideoProfile.class));
+
+        mConnectionServiceFixtureA.sendSetVideoProvider(
+                mConnectionServiceFixtureA.mLatestConnectionId);
+        InCallService.VideoCall videoCall =
+                mInCallServiceFixtureX.getCall(callIds.mCallId).getVideoCallImpl();
+        videoCall.registerCallback(callback);
+        ((VideoCallImpl) videoCall).setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+
+        videoCall.sendSessionModifyRequest(new VideoProfile(VideoProfile.STATE_RX_ENABLED));
+        counter.await(10000, TimeUnit.MILLISECONDS);
+
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        Analytics.dumpToEncodedProto(pw, new String[]{});
+        TelecomLogClass.TelecomLog analyticsProto =
+                TelecomLogClass.TelecomLog.parseFrom(Base64.decode(sw.toString(), Base64.DEFAULT));
+
+        assertEquals(1, analyticsProto.callLogs.length);
+        TelecomLogClass.VideoEvent[] videoEvents = analyticsProto.callLogs[0].videoEvents;
+        assertEquals(2, videoEvents.length);
+
+        assertEquals(Analytics.SEND_LOCAL_SESSION_MODIFY_REQUEST, videoEvents[0].getEventName());
+        assertEquals(VideoProfile.STATE_RX_ENABLED, videoEvents[0].getVideoState());
+        assertEquals(-1, videoEvents[0].getTimeSinceLastEventMillis());
+
+        assertEquals(Analytics.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE,
+                videoEvents[1].getEventName());
+        assertEquals(VideoProfile.STATE_RX_ENABLED, videoEvents[1].getVideoState());
+        assertIsRoundedToOneSigFig(videoEvents[1].getTimeSinceLastEventMillis());
+    }
+
+    @SmallTest
+    public void testAnalyticsRounding() {
+        long[] testVals = {0, -1, -10, -100, -57836, 1, 10, 100, 1000, 458457};
+        long[] expected = {0, -1, -10, -100, -60000, 1, 10, 100, 1000, 500000};
+        for (int i = 0; i < testVals.length; i++) {
+            assertEquals(expected[i], Analytics.roundToOneSigFig(testVals[i]));
+        }
+    }
+
+    @SmallTest
+    public void testAnalyticsLogSessionTiming() throws Exception {
+        long minTime = 50;
+        Log.startSession(Log.Sessions.CSW_ADD_CONFERENCE_CALL);
+        Thread.sleep(minTime);
+        Log.endSession();
+        TelecomManager tm = (TelecomManager) mSpyContext.getSystemService(Context.TELECOM_SERVICE);
+        List<TelecomAnalytics.SessionTiming> sessions = tm.dumpAnalytics().getSessionTimings();
+        sessions.stream()
+                .filter(s -> Log.Sessions.CSW_ADD_CONFERENCE_CALL.equals(
+                        Analytics.sSessionIdToLogSession.get(s.getKey())))
+                .forEach(s -> assertTrue(s.getTime() >= minTime));
+    }
+
+    @MediumTest
+    public void testAnalyticsDumpToProto() throws Exception {
+        Analytics.reset();
+        IdPair testCall = startAndMakeActiveIncomingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+        Analytics.CallInfoImpl expectedAnalytics = Analytics.cloneData().get(testCall.mCallId);
+
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        Analytics.dumpToEncodedProto(pw, new String[]{});
+        TelecomLogClass.TelecomLog analyticsProto =
+                TelecomLogClass.TelecomLog.parseFrom(Base64.decode(sw.toString(), Base64.DEFAULT));
+
+        assertEquals(1, analyticsProto.callLogs.length);
+        TelecomLogClass.CallLog callLog = analyticsProto.callLogs[0];
+
+        assertTrue(Math.abs(expectedAnalytics.startTime - callLog.getStartTime5Min()) <
+                ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+        assertEquals(0, callLog.getStartTime5Min() % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+        assertTrue(Math.abs((expectedAnalytics.endTime - expectedAnalytics.startTime) -
+                callLog.getCallDurationMillis()) < ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+        assertEquals(0,
+                callLog.getCallDurationMillis() % ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+
+        assertEquals(expectedAnalytics.callDirection, callLog.getType());
+        assertEquals(expectedAnalytics.isAdditionalCall, callLog.getIsAdditionalCall());
+        assertEquals(expectedAnalytics.isInterrupted, callLog.getIsInterrupted());
+        assertEquals(expectedAnalytics.callTechnologies, callLog.getCallTechnologies());
+        assertEquals(expectedAnalytics.callTerminationReason.getCode(),
+                callLog.getCallTerminationCode());
+        assertEquals(expectedAnalytics.connectionService, callLog.connectionService[0]);
+        TelecomLogClass.Event[] analyticsEvents = callLog.callEvents;
+        Set<Integer> capturedEvents = new HashSet<>();
+        for (TelecomLogClass.Event e : analyticsEvents) {
+            capturedEvents.add(e.getEventName());
+            assertIsRoundedToOneSigFig(e.getTimeSinceLastEventMillis());
+        }
+        assertTrue(capturedEvents.contains(ParcelableCallAnalytics.AnalyticsEvent.SET_ACTIVE));
+        assertTrue(capturedEvents.contains(
+                ParcelableCallAnalytics.AnalyticsEvent.FILTERING_INITIATED));
+    }
+
+    @MediumTest
+    public void testAnalyticsConnectionProperties() throws Exception {
+        Analytics.reset();
+        IdPair testCall = startAndMakeActiveIncomingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        int properties1 = Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE
+                | Connection.PROPERTY_WIFI
+                | Connection.PROPERTY_EMERGENCY_CALLBACK_MODE;
+        int properties2 = Connection.PROPERTY_HIGH_DEF_AUDIO
+                | Connection.PROPERTY_WIFI;
+        int expectedProperties = properties1 | properties2;
+
+        mConnectionServiceFixtureA.mConnectionById.get(testCall.mConnectionId).properties =
+                properties1;
+        mConnectionServiceFixtureA.sendSetConnectionProperties(testCall.mConnectionId);
+        mConnectionServiceFixtureA.mConnectionById.get(testCall.mConnectionId).properties =
+                properties2;
+        mConnectionServiceFixtureA.sendSetConnectionProperties(testCall.mConnectionId);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        Analytics.dumpToEncodedProto(pw, new String[]{});
+        TelecomLogClass.TelecomLog analyticsProto =
+                TelecomLogClass.TelecomLog.parseFrom(Base64.decode(sw.toString(), Base64.DEFAULT));
+
+        assertEquals(expectedProperties,
+                analyticsProto.callLogs[0].getConnectionProperties() & expectedProperties);
+    }
+
+    private void assertIsRoundedToOneSigFig(long x) {
+        assertEquals(x, Analytics.roundToOneSigFig(x));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 49ff43a..1c096f9 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -27,7 +27,6 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.IContentProvider;
 import android.media.AudioManager;
@@ -43,19 +42,15 @@
 import android.telecom.ConnectionRequest;
 import android.telecom.DisconnectCause;
 import android.telecom.ParcelableCall;
-import android.telecom.ParcelableCallAnalytics;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.internal.telephony.CallerInfo;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.Analytics;
 import com.android.server.telecom.Log;
 
 import com.google.common.base.Predicate;
@@ -63,9 +58,6 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
-import java.io.StringWriter;
-import java.util.List;
-import java.util.Map;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.CyclicBarrier;
@@ -73,13 +65,6 @@
 
 import org.mockito.ArgumentCaptor;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
 /**
  * Performs various basic call tests in Telecom.
  */
@@ -137,7 +122,7 @@
         telecomManager.acceptRingingCall();
 
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .answer(ids.mCallId);
+                .answer(ids.mConnectionId);
         mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
 
         mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
@@ -165,7 +150,7 @@
 
         // Answer video API should be called
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
+                .answerVideo(eq(ids.mConnectionId), eq(VideoProfile.STATE_BIDIRECTIONAL));
         mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
 
         mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
@@ -192,7 +177,7 @@
 
         // The generic answer method on the ConnectionService is used to answer audio-only calls.
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .answer(eq(ids.mCallId));
+                .answer(eq(ids.mConnectionId));
         mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
 
         mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
@@ -220,7 +205,7 @@
 
         // Answer video API should be called
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
+                .answerVideo(eq(ids.mConnectionId), eq(VideoProfile.STATE_BIDIRECTIONAL));
         mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
         mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
     }
@@ -293,6 +278,7 @@
                 .createConnection(any(PhoneAccountHandle.class), anyString(),
                         any(ConnectionRequest.class), eq(true), eq(false));
 
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         assertEquals(1, mCallerInfoAsyncQueryFactoryFixture.mRequests.size());
         for (CallerInfoAsyncQueryFactoryFixture.Request request :
                 mCallerInfoAsyncQueryFactoryFixture.mRequests) {
@@ -329,10 +315,12 @@
         mTelecomSystem.getTelecomServiceImpl().getBinder()
                 .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
 
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         verify(mConnectionServiceFixtureA.getTestDouble())
                 .createConnection(any(PhoneAccountHandle.class), anyString(),
                         any(ConnectionRequest.class), eq(true), eq(false));
 
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         // Never reply to the caller info lookup.
         assertEquals(1, mCallerInfoAsyncQueryFactoryFixture.mRequests.size());
 
@@ -372,10 +360,12 @@
         mTelecomSystem.getTelecomServiceImpl().getBinder()
                 .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
 
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         verify(mConnectionServiceFixtureA.getTestDouble())
                 .createConnection(any(PhoneAccountHandle.class), anyString(),
                         any(ConnectionRequest.class), eq(true), eq(false));
 
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         assertEquals(1, mCallerInfoAsyncQueryFactoryFixture.mRequests.size());
         for (CallerInfoAsyncQueryFactoryFixture.Request request :
                 mCallerInfoAsyncQueryFactoryFixture.mRequests) {
@@ -659,7 +649,7 @@
 
         mInCallServiceFixtureX.mInCallAdapter.sendCallEvent(ids.mCallId, TEST_EVENT, null);
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .sendCallEvent(ids.mCallId, TEST_EVENT, null);
+                .sendCallEvent(ids.mConnectionId, TEST_EVENT, null);
     }
 
     /**
@@ -680,136 +670,11 @@
         mInCallServiceFixtureX.mInCallAdapter.sendCallEvent(ids.mCallId, TEST_EVENT,
                 testBundle);
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .sendCallEvent(eq(ids.mCallId), eq(TEST_EVENT),
+                .sendCallEvent(eq(ids.mConnectionId), eq(TEST_EVENT),
                         bundleArgumentCaptor.capture());
         assert (bundleArgumentCaptor.getValue().containsKey(TEST_BUNDLE_KEY));
     }
 
-    @MediumTest
-    public void testAnalyticsSingleCall() throws Exception {
-        IdPair testCall = startAndMakeActiveIncomingCall(
-                "650-555-1212",
-                mPhoneAccountA0.getAccountHandle(),
-                mConnectionServiceFixtureA);
-        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
-
-        assertTrue(analyticsMap.containsKey(testCall.mCallId));
-
-        Analytics.CallInfoImpl callAnalytics = analyticsMap.get(testCall.mCallId);
-        assertTrue(callAnalytics.startTime > 0);
-        assertEquals(0, callAnalytics.endTime);
-        assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics.callDirection);
-        assertFalse(callAnalytics.isInterrupted);
-        assertNull(callAnalytics.callTerminationReason);
-        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
-                callAnalytics.connectionService);
-
-        mConnectionServiceFixtureA.
-                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
-
-        analyticsMap = Analytics.cloneData();
-        callAnalytics = analyticsMap.get(testCall.mCallId);
-        assertTrue(callAnalytics.endTime > 0);
-        assertNotNull(callAnalytics.callTerminationReason);
-        assertEquals(DisconnectCause.ERROR, callAnalytics.callTerminationReason.getCode());
-
-        StringWriter sr = new StringWriter();
-        IndentingPrintWriter ip = new IndentingPrintWriter(sr, "    ");
-        Analytics.dump(ip);
-        String dumpResult = sr.toString();
-        String[] expectedFields = {"startTime", "endTime", "direction", "isAdditionalCall",
-                "isInterrupted", "callTechnologies", "callTerminationReason", "connectionService"};
-        for (String field : expectedFields) {
-            assertTrue(dumpResult.contains(field));
-        }
-    }
-
-    @SmallTest
-    public void testAnalyticsDumping() throws Exception {
-        Analytics.reset();
-        IdPair testCall = startAndMakeActiveIncomingCall(
-                "650-555-1212",
-                mPhoneAccountA0.getAccountHandle(),
-                mConnectionServiceFixtureA);
-
-        mConnectionServiceFixtureA.
-                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
-        Analytics.CallInfoImpl expectedAnalytics = Analytics.cloneData().get(testCall.mCallId);
-
-        TelecomManager tm = (TelecomManager) mSpyContext.getSystemService(Context.TELECOM_SERVICE);
-        List<ParcelableCallAnalytics> analyticsList = tm.dumpAnalytics();
-
-        assertEquals(1, analyticsList.size());
-        ParcelableCallAnalytics pCA = analyticsList.get(0);
-
-        assertTrue(Math.abs(expectedAnalytics.startTime - pCA.getStartTimeMillis()) <
-                ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
-        assertEquals(0, pCA.getStartTimeMillis() % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
-        assertTrue(Math.abs((expectedAnalytics.endTime - expectedAnalytics.startTime) -
-                pCA.getCallDurationMillis()) < ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
-        assertEquals(0, pCA.getCallDurationMillis() % ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
-
-        assertEquals(expectedAnalytics.callDirection, pCA.getCallType());
-        assertEquals(expectedAnalytics.isAdditionalCall, pCA.isAdditionalCall());
-        assertEquals(expectedAnalytics.isInterrupted, pCA.isInterrupted());
-        assertEquals(expectedAnalytics.callTechnologies, pCA.getCallTechnologies());
-        assertEquals(expectedAnalytics.callTerminationReason.getCode(),
-                pCA.getCallTerminationCode());
-        assertEquals(expectedAnalytics.connectionService, pCA.getConnectionService());
-    }
-
-    @MediumTest
-    public void testAnalyticsTwoCalls() throws Exception {
-        IdPair testCall1 = startAndMakeActiveIncomingCall(
-                "650-555-1212",
-                mPhoneAccountA0.getAccountHandle(),
-                mConnectionServiceFixtureA);
-        IdPair testCall2 = startAndMakeActiveOutgoingCall(
-                "650-555-1213",
-                mPhoneAccountA0.getAccountHandle(),
-                mConnectionServiceFixtureA);
-
-        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
-        assertTrue(analyticsMap.containsKey(testCall1.mCallId));
-        assertTrue(analyticsMap.containsKey(testCall2.mCallId));
-
-        Analytics.CallInfoImpl callAnalytics1 = analyticsMap.get(testCall1.mCallId);
-        Analytics.CallInfoImpl callAnalytics2 = analyticsMap.get(testCall2.mCallId);
-        assertTrue(callAnalytics1.startTime > 0);
-        assertTrue(callAnalytics2.startTime > 0);
-        assertEquals(0, callAnalytics1.endTime);
-        assertEquals(0, callAnalytics2.endTime);
-
-        assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics1.callDirection);
-        assertEquals(Analytics.OUTGOING_DIRECTION, callAnalytics2.callDirection);
-
-        assertTrue(callAnalytics1.isInterrupted);
-        assertTrue(callAnalytics2.isAdditionalCall);
-
-        assertNull(callAnalytics1.callTerminationReason);
-        assertNull(callAnalytics2.callTerminationReason);
-
-        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
-                callAnalytics1.connectionService);
-        assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
-                callAnalytics1.connectionService);
-
-        mConnectionServiceFixtureA.
-                sendSetDisconnected(testCall2.mConnectionId, DisconnectCause.REMOTE);
-        mConnectionServiceFixtureA.
-                sendSetDisconnected(testCall1.mConnectionId, DisconnectCause.ERROR);
-
-        analyticsMap = Analytics.cloneData();
-        callAnalytics1 = analyticsMap.get(testCall1.mCallId);
-        callAnalytics2 = analyticsMap.get(testCall2.mCallId);
-        assertTrue(callAnalytics1.endTime > 0);
-        assertTrue(callAnalytics2.endTime > 0);
-        assertNotNull(callAnalytics1.callTerminationReason);
-        assertNotNull(callAnalytics2.callTerminationReason);
-        assertEquals(DisconnectCause.ERROR, callAnalytics1.callTerminationReason.getCode());
-        assertEquals(DisconnectCause.REMOTE, callAnalytics2.callTerminationReason.getCode());
-    }
-
     private void blockNumber(String phoneNumber) throws Exception {
         blockNumberWithAnswer(phoneNumber, new Answer<Bundle>() {
             @Override
@@ -844,6 +709,44 @@
     }
 
     /**
+     * Tests to make sure that the Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY property is set on a
+     * Call that is based on a Connection with the Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY
+     * property set.
+     */
+    @MediumTest
+    public void testCdmaEnhancedPrivacyVoiceCall() throws Exception {
+        mConnectionServiceFixtureA.mConnectionServiceDelegate.mProperties =
+                Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY;
+
+        IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        assertTrue(Call.Details.hasProperty(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getProperties(),
+                Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY));
+    }
+
+    /**
+     * Tests to make sure that Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY is dropped
+     * when the Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY property is removed from the Connection.
+     */
+    @MediumTest
+    public void testDropCdmaEnhancedPrivacyVoiceCall() throws Exception {
+        mConnectionServiceFixtureA.mConnectionServiceDelegate.mProperties =
+                Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY;
+
+        IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+        mConnectionServiceFixtureA.mLatestConnection.setConnectionProperties(0);
+
+        assertFalse(Call.Details.hasProperty(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getProperties(),
+                Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY));
+    }
+
+    /**
      * Tests the {@link Call#pullExternalCall()} API.  Ensures that an external call which is
      * pullable can be pulled.
      *
@@ -865,7 +768,7 @@
         // Attempt to pull the call and verify the API call makes it through
         mInCallServiceFixtureX.mInCallAdapter.pullExternalCall(ids.mCallId);
         verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
-                .pullExternalCall(ids.mCallId);
+                .pullExternalCall(ids.mConnectionId);
     }
 
     /**
@@ -892,4 +795,54 @@
         verify(mConnectionServiceFixtureA.getTestDouble(), never())
                 .pullExternalCall(ids.mConnectionId);
     }
+
+    public void testMergeFailedAndNotifyInCallUi() throws Exception {
+        IdPair testCall1 = startAndMakeActiveOutgoingCall(
+                "650-555-1212",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+        IdPair testCall2 = startAndMakeActiveOutgoingCall(
+                "650-555-1213",
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        assertEquals(Call.STATE_ACTIVE,
+                mInCallServiceFixtureX.getCall(testCall1.mCallId).getState());
+        assertEquals(Call.STATE_ACTIVE,
+                mInCallServiceFixtureX.getCall(testCall2.mCallId).getState());
+        assertEquals(Call.STATE_ACTIVE,
+                mInCallServiceFixtureY.getCall(testCall1.mCallId).getState());
+        assertEquals(Call.STATE_ACTIVE,
+                mInCallServiceFixtureY.getCall(testCall2.mCallId).getState());
+
+        // Conference will not occur and instead will send setConferenceMergeFailed
+        ((ConnectionServiceFixture.FakeConnection)
+                mConnectionServiceFixtureA.mLatestConnection).setIsConferenceCreated(false);
+        mInCallServiceFixtureX.getInCallAdapter().conference(testCall2.mCallId, testCall1.mCallId);
+
+        verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT)).onConnectionEvent(
+                eq(testCall2.mCallId), eq(Connection.EVENT_CALL_MERGE_FAILED), any(Bundle.class));
+        verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT)).onConnectionEvent(
+                eq(testCall2.mCallId), eq(Connection.EVENT_CALL_MERGE_FAILED), any(Bundle.class));
+    }
+
+    @LargeTest
+    public void testEmergencyCallFailMoveToSecondSim() throws Exception {
+        IdPair ids = startAndMakeDialingEmergencyCall("650-555-1212",
+                mPhoneAccountE0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+        // The Emergency Call has failed on the default SIM with an ERROR Disconnect Cause. Retry
+        // with the other SIM PhoneAccount
+        IdPair newIds = triggerEmergencyRedial(mPhoneAccountE1.getAccountHandle(),
+                mConnectionServiceFixtureA, ids);
+
+        // Call should be active on the E1 PhoneAccount
+        mConnectionServiceFixtureA.sendSetActive(newIds.mConnectionId);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(newIds.mCallId).getState());
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(newIds.mCallId).getState());
+        assertEquals(mInCallServiceFixtureX.getCall(ids.mCallId).getAccountHandle(),
+                mPhoneAccountE1.getAccountHandle());
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index f9502dd..38ea10e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -18,6 +18,7 @@
 
 import android.media.AudioManager;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
@@ -32,6 +33,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public class CallAudioModeStateMachineTest extends StateMachineTestBase<CallAudioModeStateMachine> {
     private static class ModeTestParameters extends TestParameters {
@@ -100,6 +102,37 @@
         parametrizedTestStateMachine(testCases);
     }
 
+    @SmallTest
+    public void testNoFocusWhenRingerSilenced() throws Throwable {
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mAudioManager);
+        sm.setCallAudioManager(mCallAudioManager);
+        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+        waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
+
+        resetMocks();
+        when(mCallAudioManager.startRinging()).thenReturn(false);
+
+        sm.sendMessage(CallAudioModeStateMachine.NEW_RINGING_CALL,
+                new CallAudioModeStateMachine.MessageArgs(
+                        false, // hasActiveOrDialingCalls
+                        true, // hasRingingCalls
+                        false, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                ));
+        waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
+
+        assertEquals(CallAudioModeStateMachine.RING_STATE_NAME, sm.getCurrentStateName());
+
+        verify(mAudioManager, never()).requestAudioFocusForCall(anyInt(), anyInt());
+        verify(mAudioManager, never()).setMode(anyInt());
+
+        verify(mCallAudioManager, never()).stopRinging();
+
+        verify(mCallAudioManager).stopCallWaiting();
+    }
+
     private List<ModeTestParameters> generateTestCases() {
         List<ModeTestParameters> result = new ArrayList<>();
         result.add(new ModeTestParameters(
@@ -516,6 +549,7 @@
         waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
 
         resetMocks();
+        when(mCallAudioManager.startRinging()).thenReturn(true);
 
         sm.sendMessage(params.messageType, params.externalState);
         waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index c4526e4..dddf9e6 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.tests;
 
+import android.app.NotificationManager;
 import android.content.Context;
 import android.media.AudioManager;
 import android.media.IAudioService;
@@ -31,7 +32,9 @@
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.InterruptionFilterProxy;
 import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.WiredHeadsetManager;
 
 import org.mockito.ArgumentCaptor;
@@ -42,14 +45,20 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.same;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -63,27 +72,37 @@
     static class RoutingTestParameters extends TestParameters {
         public String name;
         public int initialRoute;
+        public int initialNotificationFilter;
         public int availableRoutes; // may excl. speakerphone, because that's always available
         public int speakerInteraction; // one of NONE, ON, or OFF
         public int bluetoothInteraction; // one of NONE, ON, or OFF
         public int action;
         public int expectedRoute;
         public int expectedAvailableRoutes; // also may exclude the speakerphone.
+        public int expectedNotificationFilter; // expected end notification filter.
+        public boolean isNotificationChangeExpected; // indicates whether we expect the notification
+                                                     // filter to change during the process of the
+                                                     // test.
         public boolean doesDeviceSupportEarpiece; // set to false in the case of Wear devices
         public boolean shouldRunWithFocus;
 
-        public RoutingTestParameters(String name, int initialRoute, int availableRoutes, int
-                speakerInteraction, int bluetoothInteraction, int action, int expectedRoute, int
-                expectedAvailableRoutes, boolean doesDeviceSupportEarpiece,
+        public RoutingTestParameters(String name, int initialRoute,
+                int initialNotificationFilter, int availableRoutes, int speakerInteraction,
+                int bluetoothInteraction, int action, int expectedRoute,
+                int expectedAvailableRoutes, int expectedNotificationFilter,
+                boolean isNotificationChangeExpected, boolean doesDeviceSupportEarpiece,
                 boolean shouldRunWithFocus) {
             this.name = name;
             this.initialRoute = initialRoute;
+            this.initialNotificationFilter = initialNotificationFilter;
             this.availableRoutes = availableRoutes;
             this.speakerInteraction = speakerInteraction;
             this.bluetoothInteraction = bluetoothInteraction;
             this.action = action;
             this.expectedRoute = expectedRoute;
             this.expectedAvailableRoutes = expectedAvailableRoutes;
+            this.expectedNotificationFilter = expectedNotificationFilter;
+            this.isNotificationChangeExpected = isNotificationChangeExpected;
             this.doesDeviceSupportEarpiece = doesDeviceSupportEarpiece;
             this.shouldRunWithFocus = shouldRunWithFocus;
         }
@@ -93,12 +112,15 @@
             return "RoutingTestParameters{" +
                     "name='" + name + '\'' +
                     ", initialRoute=" + initialRoute +
+                    ", initialNotificationFilter=" + initialNotificationFilter +
                     ", availableRoutes=" + availableRoutes +
                     ", speakerInteraction=" + speakerInteraction +
                     ", bluetoothInteraction=" + bluetoothInteraction +
                     ", action=" + action +
                     ", expectedRoute=" + expectedRoute +
                     ", expectedAvailableRoutes=" + expectedAvailableRoutes +
+                    ", expectedNotificationFilter= " + expectedNotificationFilter +
+                    ", isNotificationChangeExpected=" + isNotificationChangeExpected +
                     ", doesDeviceSupportEarpiece=" + doesDeviceSupportEarpiece +
                     ", shouldRunWithFocus=" + shouldRunWithFocus +
                     '}';
@@ -112,10 +134,12 @@
     @Mock WiredHeadsetManager mockWiredHeadsetManager;
     @Mock StatusBarNotifier mockStatusBarNotifier;
     @Mock Call fakeCall;
+    @Mock InterruptionFilterProxy mMockInterruptionFilterProxy;
 
     private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     private static final int TEST_TIMEOUT = 500;
     private AudioManager mockAudioManager;
+    private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
     @Override
     public void setUp() throws Exception {
@@ -132,12 +156,32 @@
         };
 
         when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+        when(mockCallsManager.getLock()).thenReturn(mLock);
         when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
         when(fakeCall.isAlive()).thenReturn(true);
+        setupInterruptionFilterMocks();
+
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
     }
 
+    private void setupInterruptionFilterMocks() {
+        // These mock implementations keep track of when the caller sets the current notification
+        // filter, and ensures the same value is returned via getCurrentInterruptionFilter.
+        final int objId = Objects.hashCode(mMockInterruptionFilterProxy);
+        when(mMockInterruptionFilterProxy.getCurrentInterruptionFilter()).thenReturn(
+                NotificationManager.INTERRUPTION_FILTER_ALL);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock i) {
+                int requestedFilter = (int) i.getArguments()[0];
+                when(mMockInterruptionFilterProxy.getCurrentInterruptionFilter()).thenReturn(
+                        requestedFilter);
+                return null;
+            }
+        }).when(mMockInterruptionFilterProxy).setInterruptionFilter(anyInt());
+    }
+
     @LargeTest
     public void testStateMachineTransitionsWithFocus() throws Throwable {
         List<RoutingTestParameters> paramList = generateTransitionTests(true);
@@ -159,6 +203,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
                 true);
 
         when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -177,7 +222,7 @@
         stateMachine.initialize(initState);
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
-                CallAudioRouteStateMachine.HAS_FOCUS);
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
         CallAudioState expectedMiddleState = new CallAudioState(false,
                 CallAudioState.ROUTE_WIRED_HEADSET,
@@ -199,17 +244,19 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
                 true);
 
         when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
         when(mockAudioManager.isSpeakerphoneOn()).thenReturn(true);
+
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
         stateMachine.initialize(initState);
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
-                CallAudioRouteStateMachine.HAS_FOCUS);
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE);
         CallAudioState expectedEndState = new CallAudioState(false,
@@ -218,18 +265,90 @@
 
         waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
         verifyNewSystemCallAudioState(initState, expectedEndState);
+        // Expecting to end up in earpiece, so we expect notifications to be filtered.
+        assertEquals(NotificationManager.INTERRUPTION_FILTER_ALARMS,
+                mMockInterruptionFilterProxy.getCurrentInterruptionFilter());
         resetMocks();
-
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
 
         waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
-
         assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
     }
 
+    @MediumTest
+    public void testBluetoothRinging() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
+                true);
+
+        when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        stateMachine.initialize(initState);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.RINGING_FOCUS);
+        waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
+
+        verify(mockBluetoothManager, never()).connectBluetoothAudio();
+        // Shouldn't change interruption filter when in bluetooth route.
+        assertEquals(NotificationManager.INTERRUPTION_FILTER_ALL,
+                mMockInterruptionFilterProxy.getCurrentInterruptionFilter());
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
+        verify(mockBluetoothManager, times(1)).connectBluetoothAudio();
+    }
+
+    @MediumTest
+    public void testConnectBluetoothDuringRinging() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
+                true);
+
+        when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(false);
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE);
+        stateMachine.initialize(initState);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.RINGING_FOCUS);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
+        waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
+        verify(mockBluetoothManager, never()).connectBluetoothAudio();
+        CallAudioState expectedEndState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        verifyNewSystemCallAudioState(initState, expectedEndState);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
+        verify(mockBluetoothManager, times(1)).connectBluetoothAudio();
+    }
+
     @SmallTest
     public void testInitializationWithEarpieceNoHeadsetNoBluetooth() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
@@ -303,6 +422,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
                 doesDeviceSupportEarpiece);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
@@ -313,12 +433,15 @@
         params.add(new RoutingTestParameters(
                 "Connect headset during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -326,12 +449,15 @@
         params.add(new RoutingTestParameters(
                 "Connect headset during bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 NONE, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -339,12 +465,15 @@
         params.add(new RoutingTestParameters(
                 "Connect headset during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 OFF, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -352,12 +481,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect headset during headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -365,12 +497,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect headset during headset with bluetooth available", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -378,12 +513,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect headset during bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -391,12 +529,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect headset during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -404,12 +545,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect headset during speakerphone with bluetooth available", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -417,12 +561,15 @@
         params.add(new RoutingTestParameters(
                 "Connect bluetooth during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 NONE, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -430,12 +577,15 @@
         params.add(new RoutingTestParameters(
                 "Connect bluetooth during wired headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
                 NONE, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvai
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -443,12 +593,15 @@
         params.add(new RoutingTestParameters(
                 "Connect bluetooth during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 OFF, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -456,12 +609,31 @@
         params.add(new RoutingTestParameters(
                 "Disconnect bluetooth during bluetooth without headset in", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 NONE, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
+                true, // doesDeviceSupportEarpiece
+                shouldRunWithFocus
+        ));
+
+        params.add(new RoutingTestParameters(
+                "Disconnect bluetooth during bluetooth without headset in, priority mode ", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, // initialNotificationFilter
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -469,12 +641,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect bluetooth during bluetooth with headset in", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -482,12 +657,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect bluetooth during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -495,12 +673,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect bluetooth during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 NONE, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -508,12 +689,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to speakerphone from earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 ON, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -521,12 +705,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to speakerphone from headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
                 ON, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -534,12 +721,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to speakerphone from bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 ON, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -547,12 +737,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to earpiece from bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 NONE, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -560,12 +753,47 @@
         params.add(new RoutingTestParameters(
                 "Switch to earpiece from speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
                 OFF, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALARMS, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
+                true, // doesDeviceSupportEarpiece
+                shouldRunWithFocus
+        ));
+
+        params.add(new RoutingTestParameters(
+                "Switch to earpiece from speakerphone, priority notifications", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, // initialNotificationFilter
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                OFF, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_PRIORITY, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
+                true, // doesDeviceSupportEarpiece
+                shouldRunWithFocus
+        ));
+
+        params.add(new RoutingTestParameters(
+                "Switch to earpiece from speakerphone, silent mode", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_NONE, // initialNotificationFilter
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                OFF, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_NONE, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -573,12 +801,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to bluetooth from speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 OFF, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -586,12 +817,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to bluetooth from earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 NONE, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                true, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -599,12 +833,15 @@
         params.add(new RoutingTestParameters(
                 "Switch to bluetooth from wired headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
                 NONE, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 true, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -612,12 +849,15 @@
         params.add(new RoutingTestParameters(
                 "Switch from bluetooth to wired/earpiece when neither are available", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
                 ON, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BASELINE_ROUTE, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 false, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -625,12 +865,15 @@
         params.add(new RoutingTestParameters(
                 "Disconnect wired headset when device does not support earpiece", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                NotificationManager.INTERRUPTION_FILTER_ALL, // initialNotificationFilter
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
                 ON, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_SPEAKER, // expectedAvailableRoutes
+                NotificationManager.INTERRUPTION_FILTER_ALL, // expectedNotificationFilter
+                false, // isNotificationChangeExpected
                 false, // doesDeviceSupportEarpiece
                 shouldRunWithFocus
         ));
@@ -651,6 +894,8 @@
     private void runParametrizedTestCaseWithFocus(final RoutingTestParameters params)
             throws Throwable {
         resetMocks();
+        when(mMockInterruptionFilterProxy.getCurrentInterruptionFilter()).thenReturn(
+                params.initialNotificationFilter);
 
         // Construct a fresh state machine on every case
         final CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
@@ -660,6 +905,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
                 params.doesDeviceSupportEarpiece);
 
         // Set up bluetooth and speakerphone state
@@ -668,20 +914,37 @@
         when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
                 (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
                         || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
-        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
-                params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+        doReturn(params.initialRoute == CallAudioState.ROUTE_SPEAKER).when(mockAudioManager).
+                isSpeakerphoneOn();
 
         // Set the initial CallAudioState object
         final CallAudioState initState = new CallAudioState(false,
                 params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
         stateMachine.initialize(initState);
+
         // Make the state machine have focus so that we actually do something
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
-                CallAudioRouteStateMachine.HAS_FOCUS);
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
         stateMachine.sendMessageWithSessionInfo(params.action);
 
         waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
 
+        // Capture the changes made to the interruption filter and verify that the last change
+        // made to it matches the expected interruption filter.
+        if (params.isNotificationChangeExpected) {
+            ArgumentCaptor<Integer> interruptionCaptor = ArgumentCaptor.forClass(Integer.class);
+            verify(mMockInterruptionFilterProxy, timeout(TEST_TIMEOUT).atLeastOnce())
+                    .setInterruptionFilter(interruptionCaptor.capture());
+            List<Integer> interruptionFilterValues = interruptionCaptor.getAllValues();
+
+            int lastChange = interruptionFilterValues.get(interruptionFilterValues.size() - 1)
+                    .intValue();
+            assertEquals(params.expectedNotificationFilter, lastChange);
+        } else {
+            Thread.sleep(TEST_TIMEOUT);
+            verify(mMockInterruptionFilterProxy, never()).setInterruptionFilter(anyInt());
+        }
+
         stateMachine.quitStateMachine();
 
         // Verify interactions with the speakerphone and bluetooth systems
@@ -727,6 +990,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
+                mMockInterruptionFilterProxy,
                 params.doesDeviceSupportEarpiece);
 
         // Set up bluetooth and speakerphone state
@@ -787,8 +1051,11 @@
 
     private void resetMocks() {
         reset(mockAudioManager, mockBluetoothManager, mockCallsManager,
-                mockConnectionServiceWrapper);
+                mockConnectionServiceWrapper, mMockInterruptionFilterProxy);
+        mMockInterruptionFilterProxy = mock(InterruptionFilterProxy.class);
+        setupInterruptionFilterMocks();
         when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+        when(mockCallsManager.getLock()).thenReturn(mLock);
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
     }
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index 6ca32d8..590304c 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -28,14 +28,17 @@
 import android.location.CountryListener;
 import android.net.Uri;
 import android.os.Looper;
+import android.os.PersistableBundle;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.Postsubmit;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.VideoProfile;
+import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -53,6 +56,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -195,8 +199,13 @@
     public void testDontLogCallsFromEmergencyAccount() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(EMERGENCY_ACCT_HANDLE, 0));
-        mComponentContextFixture.putBooleanResource(R.bool.allow_emergency_numbers_in_call_log,
-                false);
+        CarrierConfigManager mockCarrierConfigManager =
+                (CarrierConfigManager) mComponentContextFixture.getTestDouble()
+                        .getApplicationContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL, false);
+        when(mockCarrierConfigManager.getConfig()).thenReturn(bundle);
+
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -415,6 +424,7 @@
     }
 
     @MediumTest
+    @Postsubmit
     public void testLogCallDirectionOutgoingWithMultiUserCapability() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mOtherUserAccountHandle,
@@ -508,6 +518,7 @@
     }
 
     @MediumTest
+    @Postsubmit
     public void testLogCallDirectionOutgoingFromManagedProfile() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle, 0));
@@ -658,10 +669,13 @@
 
     private void verifyNoInsertion() {
         try {
-            verify(mContentProvider, timeout(TEST_TIMEOUT_MILLIS).never()).insert(any(String.class),
+            Thread.sleep(TEST_TIMEOUT_MILLIS);
+            verify(mContentProvider, never()).insert(any(String.class),
                     any(Uri.class), any(ContentValues.class));
         } catch (android.os.RemoteException e) {
             fail("Remote exception occurred during test execution");
+        } catch (InterruptedException e) {
+            e.printStackTrace();
         }
     }
 
@@ -669,10 +683,13 @@
     private void verifyNoInsertionInUser(int userId) {
         try {
             Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, userId);
-            verify(getContentProviderForUser(userId), timeout(TEST_TIMEOUT_MILLIS).never())
+            Thread.sleep(TEST_TIMEOUT_MILLIS);
+            verify(getContentProviderForUser(userId), never())
                     .insert(any(String.class), eq(uri), any(ContentValues.class));
         } catch (android.os.RemoteException e) {
             fail("Remote exception occurred during test execution");
+        } catch (InterruptedException e) {
+            e.printStackTrace();
         }
     }
 
@@ -721,6 +738,8 @@
         when(fakeCall.getViaNumber()).thenReturn(viaNumber);
         when(fakeCall.getInitiatingUser()).thenReturn(initiatingUser);
         when(fakeCall.getCallDataUsage()).thenReturn(callDataUsage);
+        when(fakeCall.isEmergencyCall()).thenReturn(
+                phoneAccountHandle.equals(EMERGENCY_ACCT_HANDLE));
         return fakeCall;
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
index 8ece571..f261d3e 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
@@ -21,6 +21,7 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.CallerInfoAsyncQuery;
@@ -43,6 +44,7 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
 import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -93,6 +95,16 @@
         }
     }
 
+    @SmallTest
+    public void testLookupWithEmptyHandle() {
+        CallerInfoLookupHelper.OnQueryCompleteListener listener = mock(
+                CallerInfoLookupHelper.OnQueryCompleteListener.class);
+        mCallerInfoLookupHelper.startLookup(Uri.EMPTY, listener);
+
+        verify(listener).onCallerInfoQueryComplete(eq(Uri.EMPTY), isNull(CallerInfo.class));
+        verifyProperCleanup();
+    }
+
     public void testSimpleLookup() {
         CallerInfoLookupHelper.OnQueryCompleteListener listener = mock(
                 CallerInfoLookupHelper.OnQueryCompleteListener.class);
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index fba4fb0..5ed941e 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -519,6 +519,9 @@
         for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
             ResolveInfo resolveInfo = new ResolveInfo();
             resolveInfo.serviceInfo = mServiceInfoByComponentName.get(componentName);
+            resolveInfo.serviceInfo.metaData = new Bundle();
+            resolveInfo.serviceInfo.metaData.putBoolean(
+                    TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, true);
             result.add(resolveInfo);
         }
         return result;
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index b58d2ce..255f3ea 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -102,24 +102,43 @@
         @Override
         public Connection onCreateOutgoingConnection(
                 PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
-            mLatestConnection = new FakeConnection(request.getVideoState(), request.getAddress());
-            return mLatestConnection;
+            FakeConnection fakeConnection = new FakeConnection(request.getVideoState(),
+                    request.getAddress());
+            mLatestConnection = fakeConnection;
+            if (mCapabilities != NOT_SPECIFIED) {
+                fakeConnection.setConnectionCapabilities(mCapabilities);
+            }
+            if (mProperties != NOT_SPECIFIED) {
+                fakeConnection.setConnectionProperties(mProperties);
+            }
+            return fakeConnection;
         }
 
         @Override
         public void onConference(Connection cxn1, Connection cxn2) {
-            // Usually, this is implemented by something in Telephony, which does a bunch of radio
-            // work to conference the two connections together. Here we just short-cut that and
-            // declare them conferenced.
-            Conference fakeConference = new FakeConference();
-            fakeConference.addConnection(cxn1);
-            fakeConference.addConnection(cxn2);
-            mLatestConference = fakeConference;
-            addConference(fakeConference);
+            if (((FakeConnection) cxn1).getIsConferenceCreated()) {
+                // Usually, this is implemented by something in Telephony, which does a bunch of
+                // radio work to conference the two connections together. Here we just short-cut
+                // that and declare them conferenced.
+                Conference fakeConference = new FakeConference();
+                fakeConference.addConnection(cxn1);
+                fakeConference.addConnection(cxn2);
+                mLatestConference = fakeConference;
+                addConference(fakeConference);
+            } else {
+                try {
+                    sendSetConferenceMergeFailed(cxn1.getTelecomCallId());
+                } catch (Exception e) {
+                    Log.w(this, "Exception on sendSetConferenceMergeFailed: " + e.getMessage());
+                }
+            }
         }
     }
 
     public class FakeConnection extends Connection {
+        // Set to false if you wish the Conference merge to fail.
+        boolean mIsConferenceCreated = true;
+
         public FakeConnection(int videoState, Uri address) {
             super();
             int capabilities = getConnectionCapabilities();
@@ -128,7 +147,7 @@
             capabilities |= CAPABILITY_HOLD;
             setVideoState(videoState);
             setConnectionCapabilities(capabilities);
-            setActive();
+            setDialing();
             setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
         }
 
@@ -136,6 +155,14 @@
         public void onExtrasChanged(Bundle extras) {
             mExtrasLock.countDown();
         }
+
+        public boolean getIsConferenceCreated() {
+            return mIsConferenceCreated;
+        }
+
+        public void setIsConferenceCreated(boolean isConferenceCreated) {
+            mIsConferenceCreated = isConferenceCreated;
+        }
     }
 
     public class FakeConference extends Conference {
@@ -203,6 +230,7 @@
             c.videoState = request.getVideoState();
             c.mockVideoProvider = new MockVideoProvider();
             c.videoProvider = c.mockVideoProvider.getInterface();
+            c.isConferenceCreated = true;
             mConnectionById.put(id, c);
             mConnectionServiceDelegateAdapter.createConnection(connectionManagerPhoneAccount,
                     id, request, isIncoming, isUnknown);
@@ -317,6 +345,7 @@
         int videoState;
         boolean isVoipAudioMode;
         Bundle extras;
+        boolean isConferenceCreated;
     }
 
     public class ConferenceInfo {
@@ -405,6 +434,11 @@
         }
     }
 
+    public void sendSetConnectionProperties(String id) throws Exception {
+        for (IConnectionServiceAdapter a : mConnectionServiceAdapters) {
+            a.setConnectionProperties(id, mConnectionById.get(id).properties);
+        }
+    }
     public void sendSetIsConferenced(String id) throws Exception {
         for (IConnectionServiceAdapter a : mConnectionServiceAdapters) {
             a.setIsConferenced(id, mConnectionById.get(id).conferenceId);
@@ -522,6 +556,12 @@
         }
     }
 
+    public void sendSetConferenceMergeFailed(String id) throws Exception {
+        for (IConnectionServiceAdapter a : mConnectionServiceAdapters) {
+            a.setConferenceMergeFailed(id);
+        }
+    }
+
     /**
      * Waits until the {@link Connection#onExtrasChanged(Bundle)} API has been called on a
      * {@link Connection} or {@link Conference}.
diff --git a/tests/src/com/android/server/telecom/tests/DirectToVoicemailCallFilterTest.java b/tests/src/com/android/server/telecom/tests/DirectToVoicemailCallFilterTest.java
index ac74604..2ebb6fc 100644
--- a/tests/src/com/android/server/telecom/tests/DirectToVoicemailCallFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/DirectToVoicemailCallFilterTest.java
@@ -42,7 +42,6 @@
 
     public void setUp() throws Exception {
         super.setUp();
-        when(mCall.getHandle()).thenReturn(TEST_HANDLE);
     }
 
     @SmallTest
@@ -79,13 +78,32 @@
                 ));
     }
 
+    @SmallTest
+    public void testNullResponseFromLookupHelper() {
+        CallerInfoLookupHelper.OnQueryCompleteListener queryListener = verifyLookupStart(null);
+
+        queryListener.onCallerInfoQueryComplete(null, null);
+        verify(mCallback).onCallFilteringComplete(mCall,
+                new CallFilteringResult(
+                        true, // shouldAllowCall
+                        false, // shouldReject
+                        true, // shouldAddToCallLog
+                        true // shouldShowNotification
+                ));
+    }
+
     private CallerInfoLookupHelper.OnQueryCompleteListener verifyLookupStart() {
+        return verifyLookupStart(TEST_HANDLE);
+    }
+
+    private CallerInfoLookupHelper.OnQueryCompleteListener verifyLookupStart(Uri handle) {
+        when(mCall.getHandle()).thenReturn(handle);
         DirectToVoicemailCallFilter filter =
                 new DirectToVoicemailCallFilter(mCallerInfoLookupHelper);
         filter.startFilterLookup(mCall, mCallback);
         ArgumentCaptor<CallerInfoLookupHelper.OnQueryCompleteListener> captor =
                 ArgumentCaptor.forClass(CallerInfoLookupHelper.OnQueryCompleteListener.class);
-        verify(mCallerInfoLookupHelper).startLookup(eq(TEST_HANDLE), captor.capture());
+        verify(mCallerInfoLookupHelper).startLookup(eq(handle), captor.capture());
         return captor.getValue();
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 2468774..f5c0439 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
@@ -28,15 +29,17 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.UserHandle;
-import android.telecom.ConnectionService;
 import android.telecom.InCallService;
+import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.test.mock.MockContext;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.internal.telecom.IInCallService;
+import com.android.server.telecom.Analytics;
 import com.android.server.telecom.BluetoothHeadsetProxy;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
@@ -46,22 +49,24 @@
 import com.android.server.telecom.SystemStateProvider;
 import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
 
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
+import java.util.Collections;
 import java.util.LinkedList;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyChar;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isNull;
 import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -79,6 +84,7 @@
     @Mock Resources mMockResources;
     @Mock MockContext mMockContext;
     @Mock DefaultDialerManagerAdapter mMockDefaultDialerAdapter;
+    @Mock Timeouts.Adapter mTimeoutsAdapter;
 
     private static final int CURRENT_USER_ID = 900973;
     private static final String DEF_PKG = "defpkg";
@@ -90,17 +96,18 @@
 
     private UserHandle mUserHandle = UserHandle.of(CURRENT_USER_ID);
     private InCallController mInCallController;
-    private TelecomSystem.SyncRoot mLock;
+    private TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
         MockitoAnnotations.initMocks(this);
+        when(mMockCall.getAnalytics()).thenReturn(new Analytics.CallInfo());
         doReturn(mMockResources).when(mMockContext).getResources();
         doReturn(SYS_PKG).when(mMockResources).getString(R.string.ui_default_package);
         doReturn(SYS_CLASS).when(mMockResources).getString(R.string.incall_default_class);
         mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
-                mMockSystemStateProvider, mMockDefaultDialerAdapter);
+                mMockSystemStateProvider, mMockDefaultDialerAdapter, mTimeoutsAdapter);
     }
 
     @Override
@@ -114,11 +121,9 @@
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(true);
+        when(mMockCall.isExternalCall()).thenReturn(false);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                queryIntent, PackageManager.GET_META_DATA, CURRENT_USER_ID))
-            .thenReturn(new LinkedList<ResolveInfo>());
+        setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -146,11 +151,10 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
 
         Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                queryIntent, PackageManager.GET_META_DATA, CURRENT_USER_ID))
-            .thenReturn(new LinkedList<ResolveInfo>());
+        setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -181,36 +185,18 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
                 anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -249,37 +235,20 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
                 eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
                 eq(UserHandle.CURRENT))).thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -315,40 +284,26 @@
         when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
+        when(mMockCallsManager.getAudioState()).thenReturn(null);
+        when(mMockCallsManager.canAddCall()).thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
+        when(mMockCall.getConferenceableCalls()).thenReturn(Collections.emptyList());
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(
                 any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)))
                 .thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -404,4 +359,164 @@
         assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
         assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
     }
+
+    /**
+     * Ensures that the {@link InCallController} will bind to an {@link InCallService} which
+     * supports external calls.
+     */
+    @MediumTest
+    public void testBindToService_IncludeExternal() throws Exception {
+        setupMocks(true /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
+        mInCallController.bindToServices(mMockCall);
+
+        // Query for the different InCallServices
+        ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
+                queryIntentCaptor.capture(),
+                eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
+
+        // Verify call for default dialer InCallService
+        assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
+        // Verify call for car-mode InCallService
+        assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
+        // Verify call for non-UI InCallServices
+        assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
+
+        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockContext, times(1)).bindServiceAsUser(
+                bindIntentCaptor.capture(),
+                any(ServiceConnection.class),
+                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
+                eq(UserHandle.CURRENT));
+
+        Intent bindIntent = bindIntentCaptor.getValue();
+        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+        assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
+        assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName());
+    }
+
+    /**
+     * Make sure that if a call goes away before the in-call service finishes binding and another
+     * call gets connected soon after, the new call will still be sent to the in-call service.
+     */
+    @MediumTest
+    public void testUnbindDueToCallDisconnect() throws Exception {
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
+        when(mMockCall.isIncoming()).thenReturn(true);
+        when(mMockCall.isExternalCall()).thenReturn(false);
+        when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
+                .thenReturn(DEF_PKG);
+        when(mMockContext.bindServiceAsUser(
+                any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)))
+                .thenReturn(true);
+        when(mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(any(ContentResolver.class)))
+                .thenReturn(500L);
+
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        mInCallController.bindToServices(mMockCall);
+
+        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+                ArgumentCaptor.forClass(ServiceConnection.class);
+        verify(mMockContext, times(1)).bindServiceAsUser(
+                bindIntentCaptor.capture(),
+                serviceConnectionCaptor.capture(),
+                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
+                eq(UserHandle.CURRENT));
+
+        // Pretend that the call has gone away.
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
+        mInCallController.onCallRemoved(mMockCall);
+
+        // Start the connection, make sure we don't unbind, and make sure that we don't send
+        // anything to the in-call service yet.
+        ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
+        ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS);
+        IBinder mockBinder = mock(IBinder.class);
+        IInCallService mockInCallService = mock(IInCallService.class);
+        when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
+
+        serviceConnection.onServiceConnected(defDialerComponentName, mockBinder);
+        verify(mockInCallService).setInCallAdapter(any(IInCallAdapter.class));
+        verify(mMockContext, never()).unbindService(serviceConnection);
+        verify(mockInCallService, never()).addCall(any(ParcelableCall.class));
+
+        // Now, we add in the call again and make sure that it's sent to the InCallService.
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
+        mInCallController.onCallAdded(mMockCall);
+        verify(mockInCallService).addCall(any(ParcelableCall.class));
+    }
+
+    private void setupMocks(boolean isExternalCall) {
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
+        when(mMockCall.isIncoming()).thenReturn(false);
+        when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+        when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
+                .thenReturn(DEF_PKG);
+        when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
+                anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
+        when(mMockCall.isExternalCall()).thenReturn(isExternalCall);
+    }
+
+    private ResolveInfo getDefResolveInfo(final boolean includeExternalCalls) {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = DEF_PKG;
+            serviceInfo.name = DEF_CLASS;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+            serviceInfo.metaData = new Bundle();
+            serviceInfo.metaData.putBoolean(
+                    TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
+            if (includeExternalCalls) {
+                serviceInfo.metaData.putBoolean(
+                        TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, true);
+            }
+        }};
+    }
+
+    private ResolveInfo getSysResolveinfo() {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = SYS_PKG;
+            serviceInfo.name = SYS_CLASS;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+        }};
+    }
+
+    private void setupMockPackageManager(final boolean useDefaultDialer,
+            final boolean useSystemDialer, final boolean includeExternalCalls) {
+
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] args = invocation.getArguments();
+                Intent intent = (Intent) args[0];
+                String packageName = intent.getPackage();
+                ComponentName componentName = intent.getComponent();
+                if (componentName != null) {
+                    packageName = componentName.getPackageName();
+                }
+                LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
+                if (!TextUtils.isEmpty(packageName)) {
+                    if ((TextUtils.isEmpty(packageName) || packageName.equals(DEF_PKG)) &&
+                            useDefaultDialer) {
+                        resolveInfo.add(getDefResolveInfo(includeExternalCalls));
+                    }
+
+                    if ((TextUtils.isEmpty(packageName) || packageName.equals(SYS_PKG)) &&
+                           useSystemDialer) {
+                        resolveInfo.add(getSysResolveinfo());
+                    }
+                }
+                return resolveInfo;
+            }
+        }).when(mMockPackageManager).queryIntentServicesAsUser(
+                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/LogTest.java b/tests/src/com/android/server/telecom/tests/LogTest.java
index 586105f..20c4d63 100644
--- a/tests/src/com/android/server/telecom/tests/LogTest.java
+++ b/tests/src/com/android/server/telecom/tests/LogTest.java
@@ -16,29 +16,33 @@
 
 package com.android.server.telecom.tests;
 
-import android.content.ContentResolver;
-import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.os.SomeArgs;
-import com.android.server.telecom.Runnable;
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.Call;
 import com.android.server.telecom.Session;
 import com.android.server.telecom.SystemLoggingContainer;
 import com.android.server.telecom.Log;
 
 import org.junit.Assert;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockitoAnnotations;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * Unit tests for Telecom's Logging system.
@@ -456,6 +460,26 @@
         assertEquals(null, sessionRef.get());
     }
 
+    @SmallTest
+    public void testEventRecordTiming() throws Exception {
+        Call call = mock(Call.class);
+        Analytics.CallInfo mockCallInfo = mock(Analytics.CallInfo.class);
+        when(call.getAnalytics()).thenReturn(mockCallInfo);
+        int minWaitTime = 40;
+        Log.event(call, Log.Events.REQUEST_ACCEPT);
+        Thread.sleep(minWaitTime);
+        Log.event(call, Log.Events.SET_ACTIVE);
+
+        ArgumentCaptor<Log.CallEventRecord> captor =
+                ArgumentCaptor.forClass(Log.CallEventRecord.class);
+        verify(mockCallInfo).setCallEvents(captor.capture());
+        List<Log.CallEventRecord.EventTiming> eventTimings =
+                captor.getValue().extractEventTimings();
+        eventTimings.stream()
+                .filter(timing -> timing.name.equals("accept"))
+                .forEach(timing -> assertTrue(timing.time > minWaitTime));
+    }
+
     private void verifyMethodCall(String parentSessionName, String methodName, int sessionId,
             String subsession, String shortMethodName, int timeoutMs) {
         if (!parentSessionName.isEmpty()){
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 12e7280..2663356 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -43,6 +43,7 @@
 import com.android.server.telecom.Constants;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index a7d98db..54613d4 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -38,6 +38,7 @@
 import com.android.server.telecom.NewOutgoingCallIntentBroadcaster;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
+import com.android.server.telecom.TelecomSystem;
 
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -77,6 +78,7 @@
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mPhoneNumberUtilsAdapterSpy = spy(new PhoneNumberUtilsAdapterImpl());
         when(mCall.getInitiatingUser()).thenReturn(UserHandle.CURRENT);
+        when(mCallsManager.getLock()).thenReturn(new TelecomSystem.SyncRoot() { });
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index ceabc58..12d24c4 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -171,8 +171,7 @@
     public void testAccounts() throws Exception {
         int i = 0;
 
-        mComponentContextFixture.addConnectionService(
-                makeQuickConnectionServiceComponentName(),
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
                 Mockito.mock(IConnectionService.class));
 
         registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++)
@@ -205,8 +204,7 @@
 
     @MediumTest
     public void testDefaultOutgoing() throws Exception {
-        mComponentContextFixture.addConnectionService(
-                makeQuickConnectionServiceComponentName(),
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
                 Mockito.mock(IConnectionService.class));
 
         // By default, there is no default outgoing account (nothing has been registered)
@@ -253,6 +251,229 @@
     }
 
     @MediumTest
+    public void testReplacePhoneAccountByGroup() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        // By default, there is no default outgoing account (nothing has been registered)
+        assertNull(
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL));
+
+        // Register one tel: account
+        PhoneAccountHandle telAccount1 = makeQuickAccountHandle("tel_acct1");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(telAccount1, Process.myUserHandle());
+        PhoneAccountHandle defaultAccount =
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL);
+        assertEquals(telAccount1, defaultAccount);
+
+        // Add call capable SIP account, make sure tel: doesn't change
+        PhoneAccountHandle sipAccount = makeQuickAccountHandle("sip_acct");
+        registerAndEnableAccount(new PhoneAccount.Builder(sipAccount, "sip_acct")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .build());
+        defaultAccount = mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                PhoneAccount.SCHEME_TEL);
+        assertEquals(telAccount1, defaultAccount);
+
+        // Replace tel: account with another in the same Group
+        PhoneAccountHandle telAccount2 = makeQuickAccountHandle("tel_acct2");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount2, "tel_acct2")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        defaultAccount = mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                PhoneAccount.SCHEME_TEL);
+        assertEquals(telAccount2, defaultAccount);
+        assertNull(mRegistrar.getPhoneAccountUnchecked(telAccount1));
+    }
+
+    @MediumTest
+    public void testAddSameDefault() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        // By default, there is no default outgoing account (nothing has been registered)
+        assertNull(
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL));
+
+        // Register one tel: account
+        PhoneAccountHandle telAccount1 = makeQuickAccountHandle("tel_acct1");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(telAccount1, Process.myUserHandle());
+        PhoneAccountHandle defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount1, defaultAccount);
+        mRegistrar.unregisterPhoneAccount(telAccount1);
+
+        // Register Emergency Account and unregister
+        PhoneAccountHandle emerAccount = makeQuickAccountHandle("emer_acct");
+        registerAndEnableAccount(new PhoneAccount.Builder(emerAccount, "emer_acct")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .build());
+        defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertNull(defaultAccount);
+        mRegistrar.unregisterPhoneAccount(emerAccount);
+
+        // Re-register the same account and make sure the default is in place
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount1, defaultAccount);
+    }
+
+    @MediumTest
+    public void testAddSameGroup() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        // By default, there is no default outgoing account (nothing has been registered)
+        assertNull(
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL));
+
+        // Register one tel: account
+        PhoneAccountHandle telAccount1 = makeQuickAccountHandle("tel_acct1");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(telAccount1, Process.myUserHandle());
+        PhoneAccountHandle defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount1, defaultAccount);
+        mRegistrar.unregisterPhoneAccount(telAccount1);
+
+        // Register Emergency Account and unregister
+        PhoneAccountHandle emerAccount = makeQuickAccountHandle("emer_acct");
+        registerAndEnableAccount(new PhoneAccount.Builder(emerAccount, "emer_acct")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .build());
+        defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertNull(defaultAccount);
+        mRegistrar.unregisterPhoneAccount(emerAccount);
+
+        // Re-register a new account with the same group
+        PhoneAccountHandle telAccount2 = makeQuickAccountHandle("tel_acct2");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount2, "tel_acct2")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount2, defaultAccount);
+    }
+
+    @MediumTest
+    public void testAddSameGroupButDifferentComponent() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        // By default, there is no default outgoing account (nothing has been registered)
+        assertNull(mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                PhoneAccount.SCHEME_TEL));
+
+        // Register one tel: account
+        PhoneAccountHandle telAccount1 = makeQuickAccountHandle("tel_acct1");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(telAccount1, Process.myUserHandle());
+        PhoneAccountHandle defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount1, defaultAccount);
+        assertNotNull(mRegistrar.getPhoneAccountUnchecked(telAccount1));
+
+        // Register a new account with the same group, but different Component, so don't replace
+        // Default
+        PhoneAccountHandle telAccount2 =  makeQuickAccountHandle(
+                new ComponentName("other1", "other2"), "tel_acct2");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount2, "tel_acct2")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        assertNotNull(mRegistrar.getPhoneAccountUnchecked(telAccount2));
+
+        defaultAccount =
+                mRegistrar.getUserSelectedOutgoingPhoneAccount(Process.myUserHandle());
+        assertEquals(telAccount1, defaultAccount);
+    }
+
+    @MediumTest
+    public void testAddSameGroupButDifferentComponent2() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        // By default, there is no default outgoing account (nothing has been registered)
+        assertNull(mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                PhoneAccount.SCHEME_TEL));
+
+        // Register first tel: account
+        PhoneAccountHandle telAccount1 =  makeQuickAccountHandle(
+                new ComponentName("other1", "other2"), "tel_acct1");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount1, "tel_acct1")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+        assertNotNull(mRegistrar.getPhoneAccountUnchecked(telAccount1));
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(telAccount1, Process.myUserHandle());
+
+        // Register second account with the same group, but a second Component, so don't replace
+        // Default
+        PhoneAccountHandle telAccount2 = makeQuickAccountHandle("tel_acct2");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount2, "tel_acct2")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+
+        PhoneAccountHandle defaultAccount =
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL);
+        assertEquals(telAccount1, defaultAccount);
+
+        // Register third account with the second component name, but same group id
+        PhoneAccountHandle telAccount3 = makeQuickAccountHandle("tel_acct3");
+        registerAndEnableAccount(new PhoneAccount.Builder(telAccount3, "tel_acct3")
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .setGroupId("testGroup")
+                .build());
+
+        // Make sure that the default account is still the original PhoneAccount and that the
+        // second PhoneAccount with the second ComponentName was replaced by the third PhoneAccount
+        defaultAccount =
+                mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL);
+        assertEquals(telAccount1, defaultAccount);
+
+        assertNotNull(mRegistrar.getPhoneAccountUnchecked(telAccount1));
+        assertNull(mRegistrar.getPhoneAccountUnchecked(telAccount2));
+        assertNotNull(mRegistrar.getPhoneAccountUnchecked(telAccount3));
+    }
+
+    @MediumTest
     public void testPhoneAccountParceling() throws Exception {
         PhoneAccountHandle handle = makeQuickAccountHandle("foo");
         roundTripPhoneAccount(new PhoneAccount.Builder(handle, null).build());
@@ -268,6 +489,7 @@
                         .setShortDescription("short description")
                         .setSubscriptionAddress(Uri.parse("tel:2345678"))
                         .setSupportedUriSchemes(Arrays.asList("tel", "sip"))
+                        .setGroupId("testGroup")
                         .build());
         roundTripPhoneAccount(
                 new PhoneAccount.Builder(handle, "foo")
@@ -281,6 +503,7 @@
                         .setShortDescription("short description")
                         .setSubscriptionAddress(Uri.parse("tel:2345678"))
                         .setSupportedUriSchemes(Arrays.asList("tel", "sip"))
+                        .setGroupId("testGroup")
                         .build());
     }
 
@@ -291,10 +514,11 @@
     }
 
     private static PhoneAccountHandle makeQuickAccountHandle(String id) {
-        return new PhoneAccountHandle(
-                makeQuickConnectionServiceComponentName(),
-                id,
-                Process.myUserHandle());
+        return makeQuickAccountHandle(makeQuickConnectionServiceComponentName(), id);
+    }
+
+    private static PhoneAccountHandle makeQuickAccountHandle(ComponentName name, String id) {
+        return new PhoneAccountHandle(name, id, Process.myUserHandle());
     }
 
     private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
@@ -457,7 +681,8 @@
                 new ComponentName("pkg0", "cls0"), "id0");
         UserHandle userHandle = phoneAccountHandle.getUserHandle();
         s.defaultOutgoingAccountHandles
-                .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle));
+                .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
+                        "testGroup"));
         return s;
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 98231ce..8de54bf 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -53,7 +53,9 @@
 
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
+import org.mockito.Matchers;
 import org.mockito.Mock;
+import org.mockito.internal.matchers.VarargMatcher;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -139,6 +141,13 @@
         }
     }
 
+    private static class IntVarArgMatcher extends ArgumentMatcher<int[]> implements VarargMatcher {
+        @Override
+        public boolean matches(Object argument) {
+            return true;
+        }
+    }
+
     private ITelecomService.Stub mTSIBinder;
     private AppOpsManager mAppOpsManager;
     private UserManager mUserManager;
@@ -859,7 +868,7 @@
     public void testEndCallWithNoForegroundCall() throws Exception {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.ACTIVE);
-        when(mFakeCallsManager.getFirstCallWithState(anyInt(), anyInt(), anyInt(), anyInt()))
+        when(mFakeCallsManager.getFirstCallWithState(argThat(new IntVarArgMatcher())))
                 .thenReturn(call);
         assertTrue(mTSIBinder.endCall());
         verify(call).disconnect();
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 6535100..d098eb9 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -33,6 +33,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -44,11 +45,13 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Process;
 import android.os.UserHandle;
 import android.provider.BlockedNumberContract;
 import android.telecom.Call;
 import android.telecom.ConnectionRequest;
+import android.telecom.DisconnectCause;
 import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -59,6 +62,7 @@
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.BluetoothPhoneServiceImpl;
 import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallerInfoAsyncQueryFactory;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
@@ -68,12 +72,17 @@
 import com.android.server.telecom.HeadsetMediaButtonFactory;
 import com.android.server.telecom.InCallWakeLockController;
 import com.android.server.telecom.InCallWakeLockControllerFactory;
+import com.android.server.telecom.InterruptionFilterProxy;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
 import com.android.server.telecom.ProximitySensorManager;
 import com.android.server.telecom.ProximitySensorManagerFactory;
+import com.android.server.telecom.Runnable;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callfiltering.AsyncBlockCheckFilter;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
 
@@ -86,6 +95,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Implements mocks and functionality required to implement telecom system tests.
@@ -146,11 +157,39 @@
     }
 
     MissedCallNotifierFakeImpl mMissedCallNotifier = new MissedCallNotifierFakeImpl();
+    private class EmergencyNumberUtilsAdapter extends PhoneNumberUtilsAdapterImpl {
+
+        @Override
+        public boolean isLocalEmergencyNumber(Context context, String number) {
+            return mIsEmergencyCall;
+        }
+
+        @Override
+        public boolean isPotentialLocalEmergencyNumber(Context context, String number) {
+            return mIsEmergencyCall;
+        }
+    }
+    PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new EmergencyNumberUtilsAdapter();
+
+    public static class MockInterruptionFilterProxy implements InterruptionFilterProxy {
+        private int mInterruptionFilter = NotificationManager.INTERRUPTION_FILTER_ALL;
+        @Override
+        public void setInterruptionFilter(int interruptionFilter) {
+            mInterruptionFilter = interruptionFilter;
+        }
+
+        @Override
+        public int getCurrentInterruptionFilter() {
+            return mInterruptionFilter;
+        }
+    }
+
     @Mock HeadsetMediaButton mHeadsetMediaButton;
     @Mock ProximitySensorManager mProximitySensorManager;
     @Mock InCallWakeLockController mInCallWakeLockController;
     @Mock BluetoothPhoneServiceImpl mBluetoothPhoneServiceImpl;
     @Mock AsyncRingtonePlayer mAsyncRingtonePlayer;
+    @Mock InterruptionFilterProxy mInterruptionFilterProxy;
 
     final ComponentName mInCallServiceComponentNameX =
             new ComponentName(
@@ -182,7 +221,8 @@
                     .addSupportedUriScheme("tel")
                     .setCapabilities(
                             PhoneAccount.CAPABILITY_CALL_PROVIDER |
-                            PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING)
                     .build();
     final PhoneAccount mPhoneAccountA1 =
             PhoneAccount.builder(
@@ -193,7 +233,8 @@
                     .addSupportedUriScheme("tel")
                     .setCapabilities(
                             PhoneAccount.CAPABILITY_CALL_PROVIDER |
-                            PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING)
                     .build();
     final PhoneAccount mPhoneAccountB0 =
             PhoneAccount.builder(
@@ -204,7 +245,35 @@
                     .addSupportedUriScheme("tel")
                     .setCapabilities(
                             PhoneAccount.CAPABILITY_CALL_PROVIDER |
-                            PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING)
+                    .build();
+    final PhoneAccount mPhoneAccountE0 =
+            PhoneAccount.builder(
+                    new PhoneAccountHandle(
+                            mConnectionServiceComponentNameA,
+                            "id E 0"),
+                    "Phone account service E ID 0")
+                    .addSupportedUriScheme("tel")
+                    .setCapabilities(
+                            PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING)
+                    .build();
+
+    final PhoneAccount mPhoneAccountE1 =
+            PhoneAccount.builder(
+                    new PhoneAccountHandle(
+                            mConnectionServiceComponentNameA,
+                            "id E 1"),
+                    "Phone account service E ID 1")
+                    .addSupportedUriScheme("tel")
+                    .setCapabilities(
+                            PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING)
                     .build();
 
     ConnectionServiceFixture mConnectionServiceFixtureA;
@@ -221,6 +290,8 @@
 
     private int mNumOutgoingCallsMade;
 
+    private boolean mIsEmergencyCall;
+
     class IdPair {
         final String mConnectionId;
         final String mCallId;
@@ -239,6 +310,8 @@
 
         mNumOutgoingCallsMade = 0;
 
+        mIsEmergencyCall = false;
+
         // First set up information about the In-Call services in the mock Context, since
         // Telecom will search for these as soon as it is instantiated
         setupInCallServices();
@@ -298,7 +371,7 @@
 
         mTimeoutsAdapter = mock(Timeouts.Adapter.class);
         when(mTimeoutsAdapter.getCallScreeningTimeoutMillis(any(ContentResolver.class)))
-                .thenReturn(TEST_TIMEOUT / 10L);
+                .thenReturn(TEST_TIMEOUT / 5L);
 
         mTelecomSystem = new TelecomSystem(
                 mComponentContextFixture.getTestDouble(),
@@ -328,7 +401,9 @@
                     }
                 },
                 mTimeoutsAdapter,
-                mAsyncRingtonePlayer);
+                mAsyncRingtonePlayer,
+                mPhoneNumberUtilsAdapter,
+                mInterruptionFilterProxy);
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
@@ -358,6 +433,8 @@
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountA0);
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountA1);
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountB0);
+        mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE0);
+        mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE1);
 
         mTelecomSystem.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(
                 mPhoneAccountA0.getAccountHandle(), Process.myUserHandle());
@@ -461,10 +538,46 @@
                 phoneAccountHandle, connectionServiceFixture);
     }
 
-    protected String startOutgoingPhoneCallPendingCreateConnection(String number,
+    protected IdPair triggerEmergencyRedial(PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture, IdPair emergencyIds)
+            throws Exception {
+        int startingNumConnections = connectionServiceFixture.mConnectionById.size();
+        int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
+
+        // Send the message to disconnect the Emergency call due to an error.
+        // CreateConnectionProcessor should now try the second SIM account
+        connectionServiceFixture.sendSetDisconnected(emergencyIds.mConnectionId,
+                DisconnectCause.ERROR);
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(
+                emergencyIds.mCallId).getState());
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureY.getCall(
+                emergencyIds.mCallId).getState());
+
+        return redialingCallCreateConnectionComplete(startingNumConnections, startingNumCalls,
+                phoneAccountHandle, connectionServiceFixture);
+    }
+
+    protected IdPair startOutgoingEmergencyCall(String number,
             PhoneAccountHandle phoneAccountHandle,
             ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
             int videoState) throws Exception {
+        int startingNumConnections = connectionServiceFixture.mConnectionById.size();
+        int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
+
+        mIsEmergencyCall = true;
+        // Call will not use the ordered broadcaster, since it is an Emergency Call
+        startOutgoingPhoneCallWaitForBroadcaster(number, phoneAccountHandle,
+                connectionServiceFixture, initiatingUser, videoState, true /*isEmergency*/);
+
+        return outgoingCallCreateConnectionComplete(startingNumConnections, startingNumCalls,
+                phoneAccountHandle, connectionServiceFixture);
+    }
+
+    protected void startOutgoingPhoneCallWaitForBroadcaster(String number,
+            PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
+            int videoState, boolean isEmergency) throws Exception {
         reset(connectionServiceFixture.getTestDouble(), mInCallServiceFixtureX.getTestDouble(),
                 mInCallServiceFixtureY.getTestDouble());
 
@@ -480,7 +593,11 @@
         Intent actionCallIntent = new Intent();
         actionCallIntent.setData(Uri.parse("tel:" + number));
         actionCallIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
-        actionCallIntent.setAction(Intent.ACTION_CALL);
+        if(isEmergency) {
+            actionCallIntent.setAction(Intent.ACTION_CALL_EMERGENCY);
+        } else {
+            actionCallIntent.setAction(Intent.ACTION_CALL);
+        }
         if (phoneAccountHandle != null) {
             actionCallIntent.putExtra(
                     TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
@@ -508,6 +625,14 @@
                     .setInCallAdapter(
                             any(IInCallAdapter.class));
         }
+    }
+
+    protected String startOutgoingPhoneCallPendingCreateConnection(String number,
+            PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
+            int videoState) throws Exception {
+        startOutgoingPhoneCallWaitForBroadcaster(number,phoneAccountHandle,
+                connectionServiceFixture, initiatingUser, videoState, false /*isEmergency*/);
 
         ArgumentCaptor<Intent> newOutgoingCallIntent =
                 ArgumentCaptor.forClass(Intent.class);
@@ -539,6 +664,30 @@
         return mInCallServiceFixtureX.mLatestCallId;
     }
 
+    // When Telecom is redialing due to an error, we need to make sure the number of connections
+    // increase, but not the number of Calls in the InCallService.
+    protected IdPair redialingCallCreateConnectionComplete(int startingNumConnections,
+            int startingNumCalls, PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture) throws Exception {
+
+        assertEquals(startingNumConnections + 1, connectionServiceFixture.mConnectionById.size());
+
+        verify(connectionServiceFixture.getTestDouble())
+                .createConnection(eq(phoneAccountHandle), anyString(), any(ConnectionRequest.class),
+                        eq(false)/*isIncoming*/, anyBoolean());
+        // Wait for handleCreateConnectionComplete
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+        // Make sure the number of registered InCallService Calls stays the same.
+        assertEquals(startingNumCalls, mInCallServiceFixtureX.mCallById.size());
+        assertEquals(startingNumCalls, mInCallServiceFixtureY.mCallById.size());
+
+        assertEquals(mInCallServiceFixtureX.mLatestCallId, mInCallServiceFixtureY.mLatestCallId);
+
+        return new IdPair(connectionServiceFixture.mLatestConnectionId,
+                mInCallServiceFixtureX.mLatestCallId);
+    }
+
     protected IdPair outgoingCallCreateConnectionComplete(int startingNumConnections,
             int startingNumCalls, PhoneAccountHandle phoneAccountHandle,
             ConnectionServiceFixture connectionServiceFixture) throws Exception {
@@ -547,9 +696,11 @@
 
         verify(connectionServiceFixture.getTestDouble())
                 .createConnection(eq(phoneAccountHandle), anyString(), any(ConnectionRequest.class),
-                        anyBoolean(), anyBoolean());
-        connectionServiceFixture.sendHandleCreateConnectionComplete(
-                connectionServiceFixture.mLatestConnectionId);
+                        eq(false)/*isIncoming*/, anyBoolean());
+        // Wait for handleCreateConnectionComplete
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        // Wait for the callback in ConnectionService#onAdapterAttached to execute.
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
 
         assertEquals(startingNumCalls + 1, mInCallServiceFixtureX.mCallById.size());
         assertEquals(startingNumCalls + 1, mInCallServiceFixtureY.mCallById.size());
@@ -737,6 +888,20 @@
         return ids;
     }
 
+    protected IdPair startAndMakeDialingEmergencyCall(
+            String number,
+            PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture) throws Exception {
+        IdPair ids = startOutgoingEmergencyCall(number, phoneAccountHandle,
+                connectionServiceFixture, Process.myUserHandle(), VideoProfile.STATE_AUDIO_ONLY);
+
+        connectionServiceFixture.sendSetDialing(ids.mConnectionId);
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+        assertEquals(Call.STATE_DIALING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+        return ids;
+    }
+
     protected static void assertTrueWithTimeout(Predicate<Void> predicate) {
         int elapsed = 0;
         while (elapsed < TEST_TIMEOUT) {
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index c079857..0319e81 100644
--- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -18,10 +18,14 @@
 
 import org.mockito.ArgumentCaptor;
 
+import android.os.RemoteException;
 import android.telecom.CallAudioState;
 import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.Log;
 
 import java.util.List;
 
@@ -39,6 +43,7 @@
      * Tests to ensure an incoming video-call is automatically routed to the speakerphone when
      * the call is answered and neither a wired headset nor bluetooth headset are connected.
      */
+    @MediumTest
     public void testAutoSpeakerphoneIncomingBidirectional() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
@@ -54,6 +59,7 @@
      * always answer incoming video calls as bi-directional.  It is, however, possible for a third
      * party dialer to answer an incoming video call a a one-way video call.
      */
+    @MediumTest
     public void testAutoSpeakerphoneIncomingReceiveOnly() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
@@ -67,6 +73,7 @@
      * Tests audio routing for an outgoing video call made with bidirectional video.  Expect to be
      * in speaker mode.
      */
+    @MediumTest
     public void testAutoSpeakerphoneOutgoingBidirectional() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -81,6 +88,7 @@
      * in speaker mode.  Note: The default UI does not support making one-way video calls, but the
      * APIs do and a third party incall UI could choose to support that.
      */
+    @MediumTest
     public void testAutoSpeakerphoneOutgoingTransmitOnly() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -95,6 +103,7 @@
      * in speaker mode.  Note: The default UI does not support making one-way video calls, but the
      * APIs do and a third party incall UI could choose to support that.
      */
+    @MediumTest
     public void testNoAutoSpeakerphoneOnOutgoing() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -107,6 +116,7 @@
     /**
      * Tests to ensure an incoming audio-only call is routed to the earpiece.
      */
+    @MediumTest
     public void testNoAutoSpeakerphoneOnIncoming() throws Exception {
 
         // Start an incoming video call.
@@ -127,13 +137,25 @@
     private void verifyAudioRoute(int expectedRoute) throws Exception {
         // Capture all onCallAudioStateChanged callbacks to InCall.
         CallAudioRouteStateMachine carsm = mTelecomSystem.getCallsManager()
-                        .getCallAudioManager().getCallAudioRouteStateMachine();
+                .getCallAudioManager().getCallAudioRouteStateMachine();
+        CallAudioModeStateMachine camsm = mTelecomSystem.getCallsManager()
+                .getCallAudioManager().getCallAudioModeStateMachine();
+        waitForHandlerAction(camsm.getHandler(), TEST_TIMEOUT);
+        final boolean[] success = {true};
+        carsm.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> {
+            ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass(
+                    CallAudioState.class);
+            try {
+                verify(mInCallServiceFixtureX.getTestDouble(), atLeastOnce())
+                        .onCallAudioStateChanged(callAudioStateArgumentCaptor.capture());
+            } catch (RemoteException e) {
+                fail("Remote exception in InCallServiceFixture");
+            }
+            List<CallAudioState> changes = callAudioStateArgumentCaptor.getAllValues();
+            assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute());
+            success[0] = true;
+        });
         waitForHandlerAction(carsm.getHandler(), TEST_TIMEOUT);
-        ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass(
-                CallAudioState.class);
-        verify(mInCallServiceFixtureX.getTestDouble(), atLeastOnce()).onCallAudioStateChanged(
-                callAudioStateArgumentCaptor.capture());
-        List<CallAudioState> changes = callAudioStateArgumentCaptor.getAllValues();
-        assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute());
+        assertTrue(success[0]);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index bc7f9e1..993679e 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -43,6 +43,7 @@
 import android.telecom.VideoCallImpl;
 import android.telecom.VideoProfile;
 import android.telecom.VideoProfile.CameraCapabilities;
+import android.test.suitebuilder.annotation.MediumTest;
 import android.view.Surface;
 
 import com.google.common.base.Predicate;
@@ -112,6 +113,7 @@
 
         mConnectionInfo = mConnectionServiceFixtureA.mConnectionById.get(mCallIds.mConnectionId);
         mVerificationLock = new CountDownLatch(1);
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
     }
 
     @Override
@@ -124,6 +126,7 @@
      * and {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)}
      * APIS.
      */
+    @MediumTest
     public void testCameraChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -157,6 +160,7 @@
      * Tests the {@link VideoCall#setPreviewSurface(Surface)} and
      * {@link VideoProvider#onSetPreviewSurface(Surface)} APIs.
      */
+    @MediumTest
     public void testSetPreviewSurface() throws Exception {
         final Surface surface = new Surface(new SurfaceTexture(1));
         mVideoCall.setPreviewSurface(surface);
@@ -182,6 +186,7 @@
      * Tests the {@link VideoCall#setDisplaySurface(Surface)} and
      * {@link VideoProvider#onSetDisplaySurface(Surface)} APIs.
      */
+    @MediumTest
     public void testSetDisplaySurface() throws Exception {
         final Surface surface = new Surface(new SurfaceTexture(1));
         mVideoCall.setDisplaySurface(surface);
@@ -207,6 +212,7 @@
      * Tests the {@link VideoCall#setDeviceOrientation(int)} and
      * {@link VideoProvider#onSetDeviceOrientation(int)} APIs.
      */
+    @MediumTest
     public void testSetDeviceOrientation() throws Exception {
         mVideoCall.setDeviceOrientation(ORIENTATION_0);
 
@@ -230,6 +236,7 @@
     /**
      * Tests the {@link VideoCall#setZoom(float)} and {@link VideoProvider#onSetZoom(float)} APIs.
      */
+    @MediumTest
     public void testSetZoom() throws Exception {
         mVideoCall.setZoom(ZOOM_LEVEL);
 
@@ -251,6 +258,7 @@
      * Emulates a scenario where an InCallService sends a request to upgrade to video, which the
      * peer accepts as-is.
      */
+    @MediumTest
     public void testSessionModifyRequest() throws Exception {
         VideoProfile requestProfile = new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL);
 
@@ -288,6 +296,7 @@
      * Tests the {@link VideoCall#sendSessionModifyResponse(VideoProfile)},
      * and {@link VideoProvider#onSendSessionModifyResponse(VideoProfile)} APIs.
      */
+    @MediumTest
     public void testSessionModifyResponse() throws Exception {
         VideoProfile sessionModifyResponse = new VideoProfile(VideoProfile.STATE_TX_ENABLED);
 
@@ -308,6 +317,7 @@
      * {@link VideoProvider#onRequestCameraCapabilities()} ()}, and
      * {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)} APIs.
      */
+    @MediumTest
     public void testRequestCameraCapabilities() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -325,6 +335,7 @@
      * Tests the {@link VideoCall#setPauseImage(Uri)}, and
      * {@link VideoProvider#onSetPauseImage(Uri)} APIs.
      */
+    @MediumTest
     public void testSetPauseImage() throws Exception {
         final Uri testUri = Uri.fromParts("file", "test.jpg", null);
         mVideoCall.setPauseImage(testUri);
@@ -343,6 +354,7 @@
      * {@link VideoProvider#onRequestConnectionDataUsage()}, and
      * {@link VideoCall.Callback#onCallDataUsageChanged(long)} APIs.
      */
+    @MediumTest
     public void testRequestDataUsage() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -360,6 +372,7 @@
      * Tests the {@link VideoProvider#receiveSessionModifyRequest(VideoProfile)},
      * {@link VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)} APIs.
      */
+    @MediumTest
     public void testReceiveSessionModifyRequest() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -382,6 +395,7 @@
      * Tests the {@link VideoProvider#handleCallSessionEvent(int)}, and
      * {@link VideoCall.Callback#onCallSessionEvent(int)} APIs.
      */
+    @MediumTest
     public void testSessionEvent() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -400,6 +414,7 @@
      * Tests the {@link VideoProvider#changePeerDimensions(int, int)} and
      * {@link VideoCall.Callback#onPeerDimensionsChanged(int, int)} APIs.
      */
+    @MediumTest
     public void testPeerDimensionChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -419,6 +434,7 @@
      * Tests the {@link VideoProvider#changeVideoQuality(int)} and
      * {@link VideoCall.Callback#onVideoQualityChanged(int)} APIs.
      */
+    @MediumTest
     public void testVideoQualityChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)