Merge "Binds to 3rd-party InCallService with MANAGE_ONGOING_CALL permission" into rvc-qpr-dev-plus-aosp
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 9c227a6..8b7c37d 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -208,6 +208,9 @@
 
         public void setCallSource(int callSource) {
         }
+
+        public void setMissedReason(long missedReason) {
+        }
     }
 
     /**
@@ -242,6 +245,7 @@
         public List<TelecomLogClass.InCallServiceInfo> inCallServiceInfos;
         public int callProperties = 0;
         public int callSource = CALL_SOURCE_UNSPECIFIED;
+        public long missedReason;
 
         private long mTimeOfLastVideoEvent = -1;
 
@@ -254,6 +258,7 @@
             connectionService = "";
             videoEvents = new LinkedList<>();
             inCallServiceInfos = new LinkedList<>();
+            missedReason = 0;
         }
 
         CallInfoImpl(CallInfoImpl other) {
@@ -272,6 +277,7 @@
             this.videoEvents = other.videoEvents;
             this.callProperties = other.callProperties;
             this.callSource = other.callSource;
+            this.missedReason = other.missedReason;
 
             if (other.callTerminationReason != null) {
                 this.callTerminationReason = new DisconnectCause(
@@ -342,6 +348,13 @@
         }
 
         @Override
+        public void setMissedReason(long missedReason) {
+            Log.d(TAG, "setting missedReason for call " + callId + ": "
+                    + missedReason);
+            this.missedReason = missedReason;
+        }
+
+        @Override
         public void setCallEvents(EventManager.EventRecord records) {
             this.callEvents = records;
         }
@@ -399,6 +412,7 @@
                     + "    isEmergency: " + isEmergency + '\n'
                     + "    callTechnologies: " + getCallTechnologiesAsString() + '\n'
                     + "    callTerminationReason: " + getCallDisconnectReasonString() + '\n'
+                    + "    missedReason: " + getMissedReasonString() + '\n'
                     + "    connectionService: " + connectionService + '\n'
                     + "    isVideoCall: " + isVideo + '\n'
                     + "    inCallServices: " + getInCallServicesString() + '\n'
@@ -526,6 +540,11 @@
             }
         }
 
+        private String getMissedReasonString() {
+            //TODO: Implement this
+            return null;
+        }
+
         private String getInCallServicesString() {
             StringBuilder s = new StringBuilder();
             s.append("[\n");
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 6af999e..fb4f58d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom;
 
+import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -33,6 +35,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.provider.CallLog;
 import android.provider.ContactsContract.Contacts;
 import android.telecom.CallAudioState;
 import android.telecom.CallerInfo;
@@ -588,7 +591,7 @@
 
     /**
      * Indicates whether this call is using one of the
-     * {@link com.android.server.telecom.callfiltering.IncomingCallFilter.CallFilter} modules.
+     * {@link com.android.server.telecom.callfiltering.CallFilter} modules.
      */
     private boolean mIsUsingCallFiltering = false;
 
@@ -611,6 +614,16 @@
     private String mPostCallPackageName;
 
     /**
+     * Call missed information code.
+     */
+    @CallLog.Calls.MissedReason private long mMissedReason;
+
+    /**
+     * Time that this call start ringing or simulated ringing.
+     */
+    private long mStartRingTime;
+
+    /**
      * Persists the specified parameters and initializes the new instance.
      * @param context The context.
      * @param repository The connection service repository.
@@ -692,6 +705,8 @@
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mCreationTimeMillis = mClockProxy.currentTimeMillis();
+        mMissedReason = MISSED_REASON_NOT_MISSED;
+        mStartRingTime = 0;
     }
 
     /**
@@ -3870,4 +3885,20 @@
     public String getPostCallPackageName() {
         return mPostCallPackageName;
     }
+
+    public long getMissedReason() {
+        return mMissedReason;
+    }
+
+    public void setMissedReason(long missedReason) {
+        mMissedReason = missedReason;
+    }
+
+    public long getStartRingTime() {
+        return mStartRingTime;
+    }
+
+    public void setStartRingTime(long startRingTime) {
+        mStartRingTime = startRingTime;
+    }
 }
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index b19e269..cedd41c 100755
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
 
 import android.annotation.Nullable;
@@ -84,7 +85,8 @@
                 int features, PhoneAccountHandle accountHandle, long creationDate,
                 long durationInMillis, Long dataUsage, UserHandle initiatingUser, boolean isRead,
                 @Nullable LogCallCompletedListener logCallCompletedListener, int callBlockReason,
-                CharSequence callScreeningAppName, String callScreeningComponentName) {
+                CharSequence callScreeningAppName, String callScreeningComponentName,
+                long missedReason) {
             this.context = context;
             this.callerInfo = callerInfo;
             this.number = number;
@@ -103,6 +105,7 @@
             this.callBockReason = callBlockReason;
             this.callScreeningAppName = callScreeningAppName;
             this.callScreeningComponentName = callScreeningComponentName;
+            this.missedReason = missedReason;
         }
         // Since the members are accessed directly, we don't use the
         // mXxxx notation.
@@ -127,6 +130,7 @@
         public final int callBockReason;
         public final CharSequence callScreeningAppName;
         public final String callScreeningComponentName;
+        public final long missedReason;
     }
 
     private static final String TAG = CallLogManager.class.getSimpleName();
@@ -353,20 +357,25 @@
                 call.wasEverRttCall(),
                 call.wasVolte());
 
-        if (callLogType == Calls.BLOCKED_TYPE) {
+        if (result == null) {
+            // Call auto missed before filtered
+            result = new CallFilteringResult.Builder().build();
+        }
+
+        if (callLogType == Calls.BLOCKED_TYPE || callLogType == Calls.MISSED_TYPE) {
             logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber,
                     call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
                     creationTime, age, callDataUsage, call.isEmergencyCall(),
                     call.getInitiatingUser(), call.isSelfManaged(), logCallCompletedListener,
                     result.mCallBlockReason, result.mCallScreeningAppName,
-                    result.mCallScreeningComponentName);
+                    result.mCallScreeningComponentName, call.getMissedReason());
         } else {
             logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber,
                     call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
                     creationTime, age, callDataUsage, call.isEmergencyCall(),
                     call.getInitiatingUser(), call.isSelfManaged(), logCallCompletedListener,
                     Calls.BLOCK_REASON_NOT_BLOCKED, null /*callScreeningAppName*/,
-                    null /*callScreeningComponentName*/);
+                    null /*callScreeningComponentName*/, call.getMissedReason());
         }
     }
 
@@ -390,6 +399,7 @@
      * @param callBlockReason The reason why the call is blocked.
      * @param callScreeningAppName The call screening application name which block the call.
      * @param callScreeningComponentName The call screening component name which block the call.
+     * @param missedReason The encoded information about reasons for missed call.
      */
     private void logCall(
             CallerInfo callerInfo,
@@ -409,7 +419,8 @@
             @Nullable LogCallCompletedListener logCallCompletedListener,
             int callBlockReason,
             CharSequence callScreeningAppName,
-            String callScreeningComponentName) {
+            String callScreeningComponentName,
+            long missedReason) {
 
         // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
         // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
@@ -443,7 +454,7 @@
             AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits,
                     viaNumber, presentation, callType, features, accountHandle, start, duration,
                     dataUsage, initiatingUser, isRead, logCallCompletedListener, callBlockReason,
-                    callScreeningAppName, callScreeningComponentName);
+                    callScreeningAppName, callScreeningComponentName, missedReason);
             logCallAsync(args);
         } else {
           Log.d(TAG, "Not adding emergency call to call log.");
@@ -596,7 +607,7 @@
                     c.presentation, c.callType, c.features, c.accountHandle, c.timestamp,
                     c.durationInSec, c.dataUsage, userToBeInserted == null,
                     userToBeInserted, c.isRead, c.callBockReason, c.callScreeningAppName,
-                    c.callScreeningComponentName);
+                    c.callScreeningComponentName, c.missedReason);
         }
 
 
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index b53a557..6b99633 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telecom.TelecomManager.ACTION_POST_CALL;
 import static android.telecom.TelecomManager.DURATION_LONG;
 import static android.telecom.TelecomManager.DURATION_MEDIUM;
@@ -27,6 +28,9 @@
 import static android.telecom.TelecomManager.MEDIUM_CALL_TIME_MS;
 import static android.telecom.TelecomManager.SHORT_CALL_TIME_MS;
 import static android.telecom.TelecomManager.VERY_SHORT_CALL_TIME_MS;
+import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -739,6 +743,7 @@
         // Only set the incoming call as ringing if it isn't already disconnected. It is possible
         // that the connection service disconnected the call before it was even added to Telecom, in
         // which case it makes no sense to set it back to a ringing state.
+        Log.i(this, "onCallFilteringComplete");
         mGraphHandlerThreads.clear();
 
         if (incomingCall.getState() != CallState.DISCONNECTED &&
@@ -760,16 +765,17 @@
                 } else {
                     Log.i(this, "onCallFilteringCompleted: Call rejected! " +
                             "Exceeds maximum number of ringing calls.");
-                    rejectCallAndLog(incomingCall, result);
+                    incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
+                    autoMissCallAndLog(incomingCall, result);
                 }
             } else if (hasMaximumManagedDialingCalls(incomingCall)) {
                 if (shouldSilenceInsteadOfReject(incomingCall)) {
                     incomingCall.silence();
                 } else {
-
                     Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
                             "dialing calls.");
-                    rejectCallAndLog(incomingCall, result);
+                    incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_DIALING);
+                    autoMissCallAndLog(incomingCall, result);
                 }
             } else if (result.shouldScreenViaAudio) {
                 Log.i(this, "onCallFilteringCompleted: starting background audio processing");
@@ -1335,12 +1341,18 @@
             if (isConference) {
                 notifyCreateConferenceFailed(phoneAccountHandle, call);
             } else {
+                if (hasMaximumManagedRingingCalls(call)) {
+                    call.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
+                    mCallLogManager.logCall(call, Calls.MISSED_TYPE,
+                            true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
+                }
                 notifyCreateConnectionFailed(phoneAccountHandle, call);
             }
         } else if (isInEmergencyCall()) {
             // The incoming call is implicitly being rejected so the user does not get any incoming
             // call UI during an emergency call. In this case, log the call as missed instead of
             // rejected since the user did not explicitly reject.
+            call.setMissedReason(AUTO_MISSED_EMERGENCY_CALL);
             mCallLogManager.logCall(call, Calls.MISSED_TYPE,
                     true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
             if (isConference) {
@@ -3448,7 +3460,8 @@
      * Reject an incoming call and manually add it to the Call Log.
      * @param incomingCall Incoming call that has been rejected
      */
-    private void rejectCallAndLog(Call incomingCall, CallFilteringResult result) {
+    private void autoMissCallAndLog(Call incomingCall, CallFilteringResult result) {
+        incomingCall.getAnalytics().setMissedReason(incomingCall.getMissedReason());
         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
@@ -3586,6 +3599,10 @@
                         (newState == CallState.DISCONNECTED)) {
                     maybeSendPostCallScreenIntent(call);
                 }
+                if (((newState == CallState.ABORTED) || (newState == CallState.DISCONNECTED))
+                        && (call.getDisconnectCause().getCode() != DisconnectCause.MISSED)) {
+                    call.setMissedReason(MISSED_REASON_NOT_MISSED);
+                }
                 maybeShowErrorDialogOnDisconnect(call);
 
                 Trace.beginSection("onCallStateChanged");
@@ -4742,6 +4759,7 @@
      * @param call The {@link Call} which could not be added.
      */
     private void notifyCreateConnectionFailed(PhoneAccountHandle phoneAccountHandle, Call call) {
+        call.getAnalytics().setMissedReason(call.getMissedReason());
         if (phoneAccountHandle == null) {
             return;
         }
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index ec5f7ba..a64f39d 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.tests;
 
+import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -127,7 +129,8 @@
         Analytics.dump(ip);
         String dumpResult = sr.toString();
         String[] expectedFields = {"startTime", "endTime", "direction", "isAdditionalCall",
-                "isInterrupted", "callTechnologies", "callTerminationReason", "connectionService"};
+                "isInterrupted", "callTechnologies", "callTerminationReason", "connectionService",
+                "missedReason"};
         for (String field : expectedFields) {
             assertTrue(dumpResult.contains(field));
         }
@@ -200,6 +203,8 @@
         assertTrue(callAnalytics2.startTime > 0);
         assertEquals(0, callAnalytics1.endTime);
         assertEquals(0, callAnalytics2.endTime);
+        assertEquals(MISSED_REASON_NOT_MISSED, callAnalytics1.missedReason);
+        assertEquals(MISSED_REASON_NOT_MISSED, callAnalytics2.missedReason);
 
         assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics1.callDirection);
         assertEquals(Analytics.OUTGOING_DIRECTION, callAnalytics2.callDirection);
diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
new file mode 100644
index 0000000..d2c832a
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2019 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 static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
+import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.Map;
+
+public class MissedInformationTest extends TelecomSystemTest {
+    private static final int TEST_TIMEOUT_MILLIS = 1000;
+    private static final String TEST_NUMBER = "650-555-1212";
+    private static final String TEST_NUMBER_1 = "7";
+    private static final String PACKAGE_NAME = "com.android.server.telecom.tests";
+    @Mock ContentResolver mContentResolver;
+    @Mock IContentProvider mContentProvider;
+    @Mock Call mEmergencyCall;
+    @Mock Analytics.CallInfo mCallInfo;
+    private CallsManager mCallsManager;
+    private CallIntentProcessor.AdapterImpl mAdapter;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCallsManager = mTelecomSystem.getCallsManager();
+        mAdapter = new CallIntentProcessor.AdapterImpl(mCallsManager.getDefaultDialerCache());
+        when(mContentResolver.getPackageName()).thenReturn(PACKAGE_NAME);
+        when(mContentResolver.acquireProvider(any(String.class))).thenReturn(mContentProvider);
+        when(mContentProvider.call(any(String.class), any(String.class),
+                any(String.class), any(Bundle.class))).thenReturn(new Bundle());
+        doReturn(mContentResolver).when(mSpyContext).getContentResolver();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testNotMissedCall() throws Exception {
+        IdPair testCall = startAndMakeActiveIncomingCall(
+                TEST_NUMBER,
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        mConnectionServiceFixtureA.
+                sendSetDisconnected(testCall.mConnectionId, DisconnectCause.LOCAL);
+        ContentValues values = verifyInsertionWithCapture();
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+        Analytics.CallInfoImpl callAnalytics = analyticsMap.get(testCall.mCallId);
+        assertEquals(MISSED_REASON_NOT_MISSED, callAnalytics.missedReason);
+        assertEquals(MISSED_REASON_NOT_MISSED, (int) values.getAsInteger(CallLog.Calls.MISSED_REASON));
+    }
+
+    @Test
+    public void testEmergencyCallPlacing() throws Exception {
+        Analytics.dumpToParcelableAnalytics();
+        setUpEmergencyCall();
+        mCallsManager.addCall(mEmergencyCall);
+        assertTrue(mCallsManager.isInEmergencyCall());
+
+        Intent intent = new Intent();
+        intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+               mPhoneAccountA0.getAccountHandle());
+        mAdapter.processIncomingCallIntent(mCallsManager, intent);
+
+        ContentValues values = verifyInsertionWithCapture();
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+        for (Analytics.CallInfoImpl ci : analyticsMap.values()) {
+            assertEquals(AUTO_MISSED_EMERGENCY_CALL, ci.missedReason);
+        }
+        assertEquals(AUTO_MISSED_EMERGENCY_CALL,
+                (int) values.getAsInteger(CallLog.Calls.MISSED_REASON));
+    }
+
+    @Test
+    public void testMaximumDialingCalls() throws Exception {
+        Analytics.dumpToParcelableAnalytics();
+        IdPair testDialingCall = startAndMakeDialingOutgoingCall(
+                TEST_NUMBER,
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        Intent intent = new Intent();
+        intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+                mPhoneAccountA0.getAccountHandle());
+        mAdapter.processIncomingCallIntent(mCallsManager, intent);
+
+        ContentValues values = verifyInsertionWithCapture();
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+        for (String callId : analyticsMap.keySet()) {
+            if (callId.equals(testDialingCall.mCallId)) {
+                continue;
+            }
+            assertEquals(AUTO_MISSED_MAXIMUM_DIALING, analyticsMap.get(callId).missedReason);
+        }
+        assertEquals(AUTO_MISSED_MAXIMUM_DIALING,
+                (int) values.getAsInteger(CallLog.Calls.MISSED_REASON));
+    }
+
+    @Test
+    public void testMaximumRingingCalls() throws Exception {
+        Analytics.dumpToParcelableAnalytics();
+        IdPair testRingingCall = startAndMakeRingingIncomingCall(
+                TEST_NUMBER,
+                mPhoneAccountA0.getAccountHandle(),
+                mConnectionServiceFixtureA);
+
+        Intent intent = new Intent();
+        intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+                mPhoneAccountA0.getAccountHandle());
+        mAdapter.processIncomingCallIntent(mCallsManager, intent);
+
+        ContentValues values = verifyInsertionWithCapture();
+
+        Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+        for (String callId : analyticsMap.keySet()) {
+            if (callId.equals(testRingingCall.mCallId)) {
+                continue;
+            }
+            assertEquals(AUTO_MISSED_MAXIMUM_RINGING, analyticsMap.get(callId).missedReason);
+        }
+        assertEquals(AUTO_MISSED_MAXIMUM_RINGING,
+                (int) values.getAsInteger(CallLog.Calls.MISSED_REASON));
+    }
+
+    private ContentValues verifyInsertionWithCapture() {
+        ArgumentCaptor<ContentValues> captor = ArgumentCaptor.forClass(ContentValues.class);
+        verify(mContentResolver, timeout(TEST_TIMEOUT_MILLIS))
+                .insert(any(Uri.class), captor.capture());
+        return captor.getValue();
+    }
+
+    private void setUpEmergencyCall() {
+        when(mEmergencyCall.isEmergencyCall()).thenReturn(true);
+        when(mEmergencyCall.getIntentExtras()).thenReturn(new Bundle());
+        when(mEmergencyCall.getAnalytics()).thenReturn(mCallInfo);
+        when(mEmergencyCall.getState()).thenReturn(CallState.ACTIVE);
+        when(mEmergencyCall.getContext()).thenReturn(mSpyContext);
+        when(mEmergencyCall.getHandle()).thenReturn(Uri.parse("tel:" + TEST_NUMBER));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 226e7ef..aeb68c5 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -1122,6 +1122,54 @@
         return ids;
     }
 
+    protected IdPair startAndMakeDialingOutgoingCall(
+            String number,
+            PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture) throws Exception {
+        IdPair ids = startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
+                Process.myUserHandle(), VideoProfile.STATE_AUDIO_ONLY);
+
+        connectionServiceFixture.sendSetDialing(ids.mConnectionId);
+        if (phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
+            assertEquals(Call.STATE_DIALING,
+                    mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+            assertEquals(Call.STATE_DIALING,
+                    mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+        }
+
+        return ids;
+    }
+
+    protected IdPair startAndMakeRingingIncomingCall(
+            String number,
+            PhoneAccountHandle phoneAccountHandle,
+            ConnectionServiceFixture connectionServiceFixture) throws Exception {
+        IdPair ids = startIncomingPhoneCall(number, phoneAccountHandle, connectionServiceFixture);
+
+        if (phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
+            assertEquals(Call.STATE_RINGING,
+                    mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+            assertEquals(Call.STATE_RINGING,
+                    mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+            mInCallServiceFixtureX.mInCallAdapter
+                    .answerCall(ids.mCallId, VideoProfile.STATE_AUDIO_ONLY);
+
+            waitForHandlerAction(mTelecomSystem.getCallsManager()
+                    .getConnectionServiceFocusManager().getHandler(), TEST_TIMEOUT);
+
+            if (!VideoProfile.isVideo(VideoProfile.STATE_AUDIO_ONLY)) {
+                verify(connectionServiceFixture.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .answer(eq(ids.mConnectionId), any());
+            } else {
+                verify(connectionServiceFixture.getTestDouble(), timeout(TEST_TIMEOUT))
+                        .answerVideo(eq(ids.mConnectionId), eq(VideoProfile.STATE_AUDIO_ONLY),
+                                any());
+            }
+        }
+        return ids;
+    }
+
     protected static void assertTrueWithTimeout(Predicate<Void> predicate) {
         int elapsed = 0;
         while (elapsed < TEST_TIMEOUT) {