Merge "Fixes in responding call via sms"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7ace0e0..4caa4a3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -262,6 +262,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity android:name=".testapps.IncomingSelfManagedCallActivity" />
 
         <receiver android:name=".components.PrimaryCallReceiver"
                 android:exported="true"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 11b2d50..1cca867 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -175,4 +175,76 @@
     <string name="handle_restricted">RESTRICTED</string>
 
     <string name="toast_personal_call_msg">Using the personal dialer to make the call</string>
+
+    <!-- The "label" of the Notification for an incoming ringing call.
+         call_via refers to the name of the app the incoming call is coming from.
+         Example: Duo call from John Smith[CHAR LIMIT=60] -->
+    <string name="notification_incoming_call"><xliff:g id="call_via">%1$s</xliff:g> call from <xliff:g id="call_from">%2$s</xliff:g></string>
+
+    <!-- The "label" of the Notification for an incoming ringing video call.
+         call_via refers to the name of the app the incoming video call is coming from.
+         Example: Duo video call from John Smith[CHAR LIMIT=60] -->
+    <string name="notification_incoming_video_call"><xliff:g id="call_via">%1$s</xliff:g> video call from <xliff:g id="call_from">%2$s</xliff:g></string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause an ongoing call to be
+         disconnected.  This message is the part of the notification that informs the user that
+         answering the current ringing call will cause a call from another app to be disconnected.
+         Example: Answering will end your Duo call [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_call">Answering will end your <xliff:g id="call_via">%1$s</xliff:g> call</string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause other ongoing calls
+         to be disconnected.  This message is the part of the notification that informs the user
+         answering the current ringing call will cause calls from another app to be disconnected.
+         Example: Answering will end your Duo call [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_calls">Answering will end your <xliff:g id="call_via">%1$s</xliff:g> calls</string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause an ongoing video call
+         to be disconnected.  This message is the part of the notification that informs the user
+         that answering the current ringing call will cause a video call from another app to be
+         disconnected.
+         Example: Answering will end your Duo video call [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_video_call">Answering will end your <xliff:g id="call_via">%1$s</xliff:g> video call</string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause an ongoing call
+         to be disconnected.  This message is the part of the notification that informs the user
+         that answering the current ringing call will cause an ongoing call to be disconnected.
+         Similar to answering_ends_other_call, except does not specify which app.
+         [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_managed_call">Answering will end your ongoing call</string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause an ongoing call
+         to be disconnected.  This message is the part of the notification that informs the user
+         that answering the current ringing call will cause an ongoing call to be disconnected.
+         Similar to answering_ends_other_call, except does not specify which app.
+         [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_managed_calls">Answering will end your ongoing calls</string>
+
+    <!-- The "details" of a Notification for an incoming call which will cause an ongoing video call
+         to be disconnected.  This message is the part of the notification that informs the user
+         that answering the current ringing call will cause an ongoing video call to be
+         disconnected.
+         Similar to answering_ends_other_video_call, except does not specify which app.
+         [CHAR LIMIT=60] -->
+    <string name="answering_ends_other_managed_video_call">Answering will end your ongoing video call</string>
+
+    <!-- The "answer" button for an incoming call. [CHAR LIMIT=60] -->
+    <string name="answer_incoming_call">Answer</string>
+
+    <!-- The "decline" button for an incoming call. [CHAR LIMIT=60] -->
+    <string name="decline_incoming_call">Decline</string>
+
+    <!-- Error message shown to the user when an outgoing call cannot be placed due to an ongoing
+         phone call in a third-party app.  For example:
+         Call cannot be placed due to your Duo call. [CHAR LIMIT=none] -->
+    <string name="cant_call_due_to_ongoing_call">Call cannot be placed due to your <xliff:g id="other_call">%1$s</xliff:g> call.</string>
+
+    <!-- Error message shown to the user when an outgoing call cannot be placed due to ongoing
+         phone calls in a third-party app.  For example:
+         Call cannot be placed due to your Duo calls. [CHAR LIMIT=none] -->
+    <string name="cant_call_due_to_ongoing_calls">Call cannot be placed due to your <xliff:g id="other_call">%1$s</xliff:g> calls.</string>
+
+    <!-- Error message shown to the user when an outgoing call cannot be placed due to an ongoing
+         phone call in a third-party app.  Unlike cant_call_due_to_ongoing_call, this is used when
+         the name of the other app is not known. [CHAR LIMIT=none] -->
+    <string name="cant_call_due_to_ongoing_unknown_call">Call cannot be placed due to a call in another app.</string>
 </resources>
diff --git a/scripts/telecom_testing.sh b/scripts/telecom_testing.sh
index 6884f5f..e6423a3 100644
--- a/scripts/telecom_testing.sh
+++ b/scripts/telecom_testing.sh
@@ -51,15 +51,15 @@
     # Build and exit script early if build fails
 
     if [ $coverage = true ] ; then
-      emma_opt="EMMA_INSTRUMENT_STATIC=true"
+      emma_opt="EMMA_INSTRUMENT=true LOCAL_EMMA_INSTRUMENT=true EMMA_INSTRUMENT_STATIC=true"
     else
-      emma_opt="EMMA_INSTRUMENT_STATIC=false"
+      emma_opt="EMMA_INSTRUMENT=false"
     fi
 
     if [ $installwdep = true ] ; then
-      mmma -j40 "packages/services/Telecomm/tests" ${emma_opt}
+      (export ${emma_opt}; mmma -j40 "packages/services/Telecomm/tests")
     else
-      mmm "packages/services/Telecomm/tests" ${emma_opt}
+      (export ${emma_opt}; mmm "packages/services/Telecomm/tests")
     fi
     if [ $? -ne 0 ] ; then
       echo "Make failed! try using -a instead of -i if building with coverage"
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index 2173fa5..54ff22b 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -615,6 +615,12 @@
                     }
                 }
             }
+            if (conferenceCall.getState() == CallState.ON_HOLD &&
+                    conferenceCall.can(Connection.CAPABILITY_MANAGE_CONFERENCE)) {
+                // If the parent IMS CEP conference call is on hold, we should mark this call as
+                // being on hold regardless of what the other children are doing.
+                state = CALL_STATE_HELD;
+            }
         } else if (isConferenceWithNoChildren) {
             // Handle the special case of an IMS conference call without conference event package
             // support.  The call will be marked as a conference, but the conference will not have
@@ -630,7 +636,12 @@
         } else {
             addressUri = call.getHandle();
         }
+
         String address = addressUri == null ? null : addressUri.getSchemeSpecificPart();
+        if (address != null) {
+            address = PhoneNumberUtils.stripSeparators(address);
+        }
+
         int addressType = address == null ? -1 : PhoneNumberUtils.toaFromString(address);
 
         if (shouldLog) {
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index a786b55..ba89682 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -35,7 +35,6 @@
 import android.telecom.Log;
 import android.telecom.Logging.EventManager;
 import android.telecom.ParcelableConnection;
-import android.telecom.ParcelableRttCall;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.Response;
@@ -87,6 +86,8 @@
 
     private static final int RTT_PIPE_READ_SIDE_INDEX = 0;
     private static final int RTT_PIPE_WRITE_SIDE_INDEX = 1;
+
+    private static final int INVALID_RTT_REQUEST_ID = -1;
     /**
      * Listener for events on the call.
      */
@@ -123,6 +124,8 @@
         void onHoldToneRequested(Call call);
         void onConnectionEvent(Call call, String event, Bundle extras);
         void onExternalCallChanged(Call call, boolean isExternalCall);
+        void onRttInitiationFailure(Call call, int reason);
+        void onRemoteRttRequest(Call call, int requestId);
     }
 
     public abstract static class ListenerBase implements Listener {
@@ -184,13 +187,16 @@
         public boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout) {
             return false;
         }
-
         @Override
         public void onHoldToneRequested(Call call) {}
         @Override
         public void onConnectionEvent(Call call, String event, Bundle extras) {}
         @Override
         public void onExternalCallChanged(Call call, boolean isExternalCall) {}
+        @Override
+        public void onRttInitiationFailure(Call call, int reason) {}
+        @Override
+        public void onRemoteRttRequest(Call call, int requestId) {}
     }
 
     private final CallerInfoLookupHelper.OnQueryCompleteListener mCallerInfoQueryListener =
@@ -283,6 +289,8 @@
 
     private boolean mSpeakerphoneOn;
 
+    private boolean mIsDisconnectingChildCall = false;
+
     /**
      * Tracks the video states which were applicable over the duration of a call.
      * See {@link VideoProfile} for a list of valid video states.
@@ -426,6 +434,11 @@
     private int mRttMode;
 
     /**
+     * Integer indicating the remote RTT request ID that is pending a response from the user.
+     */
+    private int mPendingRttRequestId = INVALID_RTT_REQUEST_ID;
+
+    /**
      * Persists the specified parameters and initializes the new instance.
      *
      * @param context The context.
@@ -1111,7 +1124,7 @@
         if (changedProperties != 0) {
             int previousProperties = mConnectionProperties;
             mConnectionProperties = connectionProperties;
-            setIsRttCall((mConnectionProperties & Connection.PROPERTY_IS_RTT) ==
+            setRttStreams((mConnectionProperties & Connection.PROPERTY_IS_RTT) ==
                     Connection.PROPERTY_IS_RTT);
             boolean didRttChange =
                     (changedProperties & Connection.PROPERTY_IS_RTT) == Connection.PROPERTY_IS_RTT;
@@ -1168,6 +1181,19 @@
         return mWasConferencePreviouslyMerged;
     }
 
+    public boolean isDisconnectingChildCall() {
+        return mIsDisconnectingChildCall;
+    }
+
+    /**
+     * Sets whether this call is a child call.
+     */
+    private void maybeSetCallAsDisconnectingChild() {
+        if (mParentCall != null) {
+            mIsDisconnectingChildCall = true;
+        }
+    }
+
     @VisibleForTesting
     public Call getConferenceLevelActiveCall() {
         return mConferenceLevelActiveCall;
@@ -1183,7 +1209,7 @@
      *
      * @return The {@link Context}.
      */
-    Context getContext() {
+    public Context getContext() {
         return mContext;
     }
 
@@ -1391,6 +1417,7 @@
 
         // Track that the call is now locally disconnecting.
         setLocallyDisconnecting(true);
+        maybeSetCallAsDisconnectingChild();
 
         if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
                 mState == CallState.CONNECTING) {
@@ -2027,7 +2054,22 @@
         return mSpeakerphoneOn;
     }
 
-    public void setIsRttCall(boolean shouldBeRtt) {
+    public void stopRtt() {
+        if (mConnectionService != null) {
+            mConnectionService.stopRtt(this);
+        } else {
+            // If this gets called by the in-call app before the connection service is set, we'll
+            // just ignore it since it's really not supposed to happen.
+            Log.w(this, "stopRtt() called before connection service is set.");
+        }
+    }
+
+    public void sendRttRequest() {
+        setRttStreams(true);
+        mConnectionService.startRtt(this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+    }
+
+    public void setRttStreams(boolean shouldBeRtt) {
         boolean areStreamsInitialized = mInCallToConnectionServiceStreams != null
                 && mConnectionServiceToInCallStreams != null;
         if (shouldBeRtt && !areStreamsInitialized) {
@@ -2044,6 +2086,45 @@
         }
     }
 
+    public void onRttConnectionFailure(int reason) {
+        setRttStreams(false);
+        for (Listener l : mListeners) {
+            l.onRttInitiationFailure(this, reason);
+        }
+    }
+
+    public void onRemoteRttRequest() {
+        if (isRttCall()) {
+            Log.w(this, "Remote RTT request on a call that's already RTT");
+            return;
+        }
+
+        mPendingRttRequestId = mCallsManager.getNextRttRequestId();
+        for (Listener l : mListeners) {
+            l.onRemoteRttRequest(this, mPendingRttRequestId);
+        }
+    }
+
+    public void handleRttRequestResponse(int id, boolean accept) {
+        if (mPendingRttRequestId == INVALID_RTT_REQUEST_ID) {
+            Log.w(this, "Response received to a nonexistent RTT request: %d", id);
+            return;
+        }
+        if (id != mPendingRttRequestId) {
+            Log.w(this, "Response ID %d does not match expected %d", id, mPendingRttRequestId);
+            return;
+        }
+        setRttStreams(accept);
+        if (accept) {
+            Log.i(this, "RTT request %d accepted.", id);
+            mConnectionService.respondToRttRequest(
+                    this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+        } else {
+            Log.i(this, "RTT request %d rejected.", id);
+            mConnectionService.respondToRttRequest(this, null, null);
+        }
+    }
+
     public void closeRttPipes() {
         // TODO: may defer this until call is removed?
     }
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index e50fd21..1075be2 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -477,6 +477,16 @@
 
         pw.println("Foreground call:");
         pw.println(mForegroundCall);
+
+        pw.println("CallAudioModeStateMachine pending messages:");
+        pw.increaseIndent();
+        mCallAudioModeStateMachine.dumpPendingMessages(pw);
+        pw.decreaseIndent();
+
+        pw.println("CallAudioRouteStateMachine pending messages:");
+        pw.increaseIndent();
+        mCallAudioRouteStateMachine.dumpPendingMessages(pw);
+        pw.decreaseIndent();
     }
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 7121a53..3d778f2 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -24,6 +24,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.util.IState;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 
@@ -515,6 +516,10 @@
         }
     }
 
+    public void dumpPendingMessages(IndentingPrintWriter pw) {
+        getHandler().getLooper().dump(pw::println, "");
+    }
+
     @Override
     protected void onPostHandleMessage(Message msg) {
         Log.endSession();
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 377ab48..7dfd78c 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -37,6 +37,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.util.IState;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -65,6 +66,9 @@
  * mIsMuted: a boolean indicating whether the audio is muted
  */
 public class CallAudioRouteStateMachine extends StateMachine {
+    private static final String TELECOM_PACKAGE =
+            CallAudioRouteStateMachine.class.getPackage().getName();
+
     /** Direct the audio stream through the device's earpiece. */
     public static final int ROUTE_EARPIECE      = CallAudioState.ROUTE_EARPIECE;
 
@@ -167,6 +171,19 @@
                 String action = intent.getAction();
 
                 if (action.equals(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)) {
+                    // We get an this broadcast any time the notification filter is changed, even if
+                    // we are the initiator of the change.
+                    // So, we'll look at who the initiator of the manual zen rule is in the
+                    // notification manager.  If its us, then we can just exit now.
+                    String initiator =
+                            mInterruptionFilterProxy.getInterruptionModeInitiator();
+
+                    if (TELECOM_PACKAGE.equals(initiator)) {
+                        // We are the initiator of this change, so ignore it.
+                        Log.i(this, "interruptionFilterChanged - ignoring own change");
+                        return;
+                    }
+
                     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
@@ -174,6 +191,8 @@
                         mAreNotificationSuppressed =
                                 mInterruptionFilterProxy.getCurrentInterruptionFilter()
                                         == NotificationManager.INTERRUPTION_FILTER_ALARMS;
+                        Log.i(this, "interruptionFilterChanged - changing to %b",
+                                mAreNotificationSuppressed);
                     }
                 }
             } finally {
@@ -301,7 +320,7 @@
             }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
                     mAvailableRoutes);
-            setSystemAudioState(newState);
+            setSystemAudioState(newState, true);
             updateInternalCallAudioState();
         }
 
@@ -489,7 +508,7 @@
             setBluetoothOn(false);
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
                     mAvailableRoutes);
-            setSystemAudioState(newState);
+            setSystemAudioState(newState, true);
             updateInternalCallAudioState();
         }
 
@@ -671,7 +690,7 @@
             setBluetoothOn(true);
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes);
-            setSystemAudioState(newState);
+            setSystemAudioState(newState, true);
             updateInternalCallAudioState();
         }
 
@@ -805,8 +824,8 @@
                     }
                     return HANDLED;
                 case BT_AUDIO_DISCONNECT:
-                    // Ignore BT_AUDIO_DISCONNECT when ringing, since SCO audio should not be
-                    // connected.
+                    // BT SCO might be connected when in-band ringing is enabled
+                    sendInternalMessage(SWITCH_BASELINE_ROUTE);
                     return HANDLED;
                 default:
                     return NOT_HANDLED;
@@ -1300,6 +1319,10 @@
         quitNow();
     }
 
+    public void dumpPendingMessages(IndentingPrintWriter pw) {
+        getHandler().getLooper().dump(pw::println, "");
+    }
+
     /**
      * Sets whether notifications should be suppressed or not.  Used when in a call to ensure the
      * device will not vibrate due to notifications.
@@ -1529,7 +1552,15 @@
     }
 
     private int calculateBaselineRouteMessage(boolean isExplicitUserRequest) {
-        if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+        boolean isSkipEarpiece = false;
+        if (!isExplicitUserRequest) {
+            synchronized (mLock) {
+                // Check video calls to skip earpiece since the baseline for video
+                // calls should be the speakerphone route
+                isSkipEarpiece = mCallsManager.hasVideoCall();
+            }
+        }
+        if ((mAvailableRoutes & ROUTE_EARPIECE) != 0 && !isSkipEarpiece) {
             return isExplicitUserRequest ? USER_SWITCH_EARPIECE : SWITCH_EARPIECE;
         } else if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
             return isExplicitUserRequest ? USER_SWITCH_HEADSET : SWITCH_HEADSET;
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index c30641b..ced282c 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -51,7 +51,6 @@
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.AsyncEmergencyContactNotifier;
@@ -69,7 +68,6 @@
 import com.android.server.telecom.components.ErrorDialogActivity;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -177,6 +175,7 @@
      */
     private int mCallId = 0;
 
+    private int mRttRequestId = 0;
     /**
      * Stores the current foreground user.
      */
@@ -261,7 +260,8 @@
         mCallerInfoLookupHelper = new CallerInfoLookupHelper(context, mCallerInfoAsyncQueryFactory,
                 mContactsAsyncHelper, mLock);
 
-        mDtmfLocalTonePlayer = new DtmfLocalTonePlayer();
+        mDtmfLocalTonePlayer =
+                new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
         mNotificationManager = (NotificationManager) context.getSystemService(
                 Context.NOTIFICATION_SERVICE);
         CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
@@ -648,7 +648,7 @@
         return true;
     }
 
-    boolean hasVideoCall() {
+    public boolean hasVideoCall() {
         for (Call call : mCalls) {
             if (VideoProfile.isVideo(call.getVideoState())) {
                 return true;
@@ -723,7 +723,7 @@
         if (extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
             if (phoneAccount != null &&
                     phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
-                call.setIsRttCall(true);
+                call.setRttStreams(true);
             }
         }
 
@@ -799,6 +799,7 @@
                 reusedCall = pendingCall;
             } else {
                 Log.i(this, "Not reusing disconnected call %s", pendingCall);
+                callIter.remove();
                 pendingCall.disconnect();
             }
         }
@@ -968,7 +969,7 @@
                     && extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
                 if (accountToUse != null
                         && accountToUse.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
-                    call.setIsRttCall(true);
+                    call.setRttStreams(true);
                 }
             }
         }
@@ -1421,7 +1422,7 @@
                         mPhoneAccountRegistrar.getPhoneAccountUnchecked(account);
                 if (realPhoneAccount != null
                         && realPhoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
-                    call.setIsRttCall(true);
+                    call.setRttStreams(true);
                 }
             }
 
@@ -1495,8 +1496,15 @@
         removeCall(call);
         Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
         if (mLocallyDisconnectingCalls.contains(call)) {
+            boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
+            Log.v(this, "markCallAsRemoved: isDisconnectingChildCall = "
+                + isDisconnectingChildCall + "call -> %s", call);
             mLocallyDisconnectingCalls.remove(call);
-            if (foregroundCall != null && foregroundCall.getState() == CallState.ON_HOLD) {
+            // Auto-unhold the foreground call due to a locally disconnected call, except if the
+            // call which was disconnected is a member of a conference (don't want to auto un-hold
+            // the conference if we remove a member of the conference).
+            if (!isDisconnectingChildCall && foregroundCall != null
+                    && foregroundCall.getState() == CallState.ON_HOLD) {
                 foregroundCall.unhold();
             }
         } else if (foregroundCall != null &&
@@ -2044,7 +2052,7 @@
     }
 
     /**
-     * Given a {@link PhoneAccountHandle} determines if there are and managed calls.
+     * Determines if there are any managed calls.
      * @return {@code true} if there are managed calls, {@code false} otherwise.
      */
     public boolean hasManagedCalls() {
@@ -2052,6 +2060,17 @@
                 !call.isExternalCall()).count() > 0;
     }
 
+    /**
+     * Determines if there are any ongoing managed calls.
+     * @return {@code true} if there are ongoing managed calls, {@code false} otherwise.
+     */
+    public boolean hasOngoingManagedCalls() {
+        return getNumCallsWithState(
+                false /* isSelfManaged */, null /* excludeCall */,
+                null /* phoneAccountHandle */,
+                LIVE_CALL_STATES) > 0;
+    }
+
     private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
         if (hasMaximumManagedLiveCalls(call)) {
             // NOTE: If the amount of live calls changes beyond 1, this logic will probably
@@ -2262,6 +2281,12 @@
         }
     }
 
+    public int getNextRttRequestId() {
+        synchronized (mLock) {
+            return (++mRttRequestId);
+        }
+    }
+
     /**
      * Callback when foreground user is switched. We will reload missed call in all profiles
      * including the user itself. There may be chances that profiles are not started yet.
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index be54f4f..7bb747e 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,6 +23,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
@@ -771,6 +772,54 @@
                 Log.endSession();
             }
         }
+
+        @Override
+        public void onRttInitiationSuccess(String callId, Session.Info sessionInfo)
+                throws RemoteException {
+
+        }
+
+        @Override
+        public void onRttInitiationFailure(String callId, int reason, Session.Info sessionInfo)
+                throws RemoteException {
+            Log.startSession(sessionInfo, "CSW.oRIF");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.onRttConnectionFailure(reason);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void onRttSessionRemotelyTerminated(String callId, Session.Info sessionInfo)
+                throws RemoteException {
+
+        }
+
+        @Override
+        public void onRemoteRttRequest(String callId, Session.Info sessionInfo)
+                throws RemoteException {
+            Log.startSession(sessionInfo, "CSW.oRRR");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.onRemoteRttRequest();
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
     }
 
     private final Adapter mAdapter = new Adapter();
@@ -860,7 +909,8 @@
                             gatewayInfo.getOriginalAddress());
                 }
 
-                Log.addEvent(call, LogUtils.Events.START_CONNECTION, Log.piiHandle(call.getHandle()));
+                Log.addEvent(call, LogUtils.Events.START_CONNECTION,
+                        Log.piiHandle(call.getHandle()));
 
                 // For self-managed incoming calls, if there is another ongoing call Telecom is
                 // responsible for showing a UI to ask the user if they'd like to answer this
@@ -872,7 +922,7 @@
                 ConnectionRequest connectionRequest = new ConnectionRequest.Builder()
                         .setAccountHandle(call.getTargetPhoneAccount())
                         .setAddress(call.getHandle())
-                        .setExtras(call.getIntentExtras())
+                        .setExtras(extras)
                         .setVideoState(call.getVideoState())
                         .setTelecomCallId(callId)
                         .setShouldShowIncomingCallUi(shouldShowIncomingCallUI)
@@ -923,7 +973,9 @@
                             Log.piiHandle(call.getHandle()));
                     try {
                         logOutgoing("createConnectionFailed %s", callId);
-                        mServiceInterface.createConnectionFailed(callId,
+                        mServiceInterface.createConnectionFailed(
+                                call.getConnectionManagerPhoneAccount(),
+                                callId,
                                 new ConnectionRequest(
                                         call.getTargetPhoneAccount(),
                                         call.getHandle(),
@@ -1212,6 +1264,41 @@
         }
     }
 
+    void startRtt(Call call, ParcelFileDescriptor fromInCall, ParcelFileDescriptor toInCall) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("startRtt")) {
+            try {
+                logOutgoing("startRtt: %s %s %s", callId, fromInCall, toInCall);
+                mServiceInterface.startRtt(callId, fromInCall, toInCall, Log.getExternalSession());
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void stopRtt(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("stopRtt")) {
+            try {
+                logOutgoing("stopRtt: %s", callId);
+                mServiceInterface.stopRtt(callId, Log.getExternalSession());
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void respondToRttRequest(
+            Call call, ParcelFileDescriptor fromInCall, ParcelFileDescriptor toInCall) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("respondToRttRequest")) {
+            try {
+                logOutgoing("respondToRttRequest: %s %s %s", callId, fromInCall, toInCall);
+                mServiceInterface.respondToRttUpgradeRequest(
+                        callId, fromInCall, toInCall, Log.getExternalSession());
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void setServiceInterface(IBinder binder) {
@@ -1272,12 +1359,12 @@
 
     private void logIncoming(String msg, Object... params) {
         Log.d(this, "ConnectionService -> Telecom[" + mComponentName.flattenToShortString() + "]: "
-                + msg, params);
+                + msg, Log.pii(params));
     }
 
     private void logOutgoing(String msg, Object... params) {
         Log.d(this, "Telecom -> ConnectionService[" + mComponentName.flattenToShortString() + "]: "
-                + msg, params);
+                + msg, Log.pii(params));
     }
 
     private void queryRemoteConnectionServices(final UserHandle userHandle,
diff --git a/src/com/android/server/telecom/DtmfLocalTonePlayer.java b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
index 5abbf49..10551a4 100644
--- a/src/com/android/server/telecom/DtmfLocalTonePlayer.java
+++ b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
@@ -21,10 +21,13 @@
 import android.media.ToneGenerator;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Message;
 import android.provider.Settings;
 import android.telecom.Log;
+import android.telecom.Logging.Session;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
@@ -36,23 +39,108 @@
  * changes.
  */
 public class DtmfLocalTonePlayer {
-    /** Generator used to actually play the tone. */
-    private ToneGenerator mToneGenerator;
+    public static class ToneGeneratorProxy {
+        /** Generator used to actually play the tone. */
+        private ToneGenerator mToneGenerator;
+
+        public void create() {
+            if (mToneGenerator == null) {
+                try {
+                    mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
+                } catch (RuntimeException e) {
+                    Log.e(this, e, "Error creating local tone generator.");
+                    mToneGenerator = null;
+                }
+            }
+        }
+
+        public void release() {
+            if (mToneGenerator != null) {
+                mToneGenerator.release();
+                mToneGenerator = null;
+            }
+        }
+
+        public boolean isPresent() {
+            return mToneGenerator != null;
+        }
+
+        public void startTone(int toneType, int durationMs) {
+            if (mToneGenerator != null) {
+                mToneGenerator.startTone(toneType, durationMs);
+            }
+        }
+
+        public void stopTone() {
+            if (mToneGenerator != null) {
+                mToneGenerator.stopTone();
+            }
+        }
+    }
+
+    private final class ToneHandler extends Handler {
+
+        public ToneHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.obj instanceof Session) {
+                Log.continueSession((Session) msg.obj, "DLTP.TH");
+            }
+
+            switch(msg.what) {
+                case EVENT_START_SESSION:
+                    mToneGeneratorProxy.create();
+                    break;
+                case EVENT_END_SESSION:
+                    mToneGeneratorProxy.release();
+                    break;
+                case EVENT_PLAY_TONE:
+                    char c = (char) msg.arg1;
+                    if (!mToneGeneratorProxy.isPresent()) {
+                        Log.d(this, "playTone: no tone generator, %c.", c);
+                    } else {
+                        Log.d(this, "starting local tone: %c.", c);
+                        int tone = getMappedTone(c);
+                        if (tone != ToneGenerator.TONE_UNKNOWN) {
+                            mToneGeneratorProxy.startTone(tone, -1 /* toneDuration */);
+                        }
+                    }
+                    break;
+                case EVENT_STOP_TONE:
+                    if (mToneGeneratorProxy.isPresent()) {
+                        mToneGeneratorProxy.stopTone();
+                    }
+                    break;
+                default:
+                    Log.w(this, "Unknown message: %d", msg.what);
+                    break;
+            }
+        }
+    }
 
     /** The current call associated with an existing dtmf session. */
     private Call mCall;
 
     /**
      * Message codes to be used for creating and deleting ToneGenerator object in the tonegenerator
-     * thread.
+     * thread, as well as for actually playing the tones.
      */
-    private static final int EVENT_CREATE_OBJECT = 1;
-    private static final int EVENT_DELETE_OBJECT = 2;
+    private static final int EVENT_START_SESSION = 1;
+    private static final int EVENT_END_SESSION = 2;
+    private static final int EVENT_PLAY_TONE = 3;
+    private static final int EVENT_STOP_TONE = 4;
 
     /** Handler running on the tonegenerator thread. */
-    private Handler mHandler;
+    private ToneHandler mHandler;
 
-    public DtmfLocalTonePlayer() { }
+    private final ToneGeneratorProxy mToneGeneratorProxy;
+
+    public DtmfLocalTonePlayer(ToneGeneratorProxy toneGeneratorProxy) {
+        mToneGeneratorProxy = toneGeneratorProxy;
+    }
 
     public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
         endDtmfSession(oldForegroundCall);
@@ -65,22 +153,14 @@
      * @param call The associated call.
      * @param c The digit to play.
      */
-    void playTone(Call call, char c) {
+    public void playTone(Call call, char c) {
         // Do nothing if it is not the right call.
         if (mCall != call) {
             return;
         }
-        synchronized(this) {
-            if (mToneGenerator == null) {
-                Log.d(this, "playTone: mToneGenerator == null, %c.", c);
-            } else {
-                Log.d(this, "starting local tone: %c.", c);
-                int tone = getMappedTone(c);
-                if (tone != ToneGenerator.TONE_UNKNOWN) {
-                    mToneGenerator.startTone(tone, -1 /* toneDuration */);
-                }
-            }
-        }
+
+        getHandler().sendMessage(
+                getHandler().obtainMessage(EVENT_PLAY_TONE, (int) c, 0, Log.createSubsession()));
     }
 
     /**
@@ -88,19 +168,14 @@
      *
      * @param call The associated call.
      */
-    void stopTone(Call call) {
+    public void stopTone(Call call) {
         // Do nothing if it's not the right call.
         if (mCall != call) {
             return;
         }
-        synchronized(this) {
-            if (mToneGenerator == null) {
-                Log.d(this, "stopTone: mToneGenerator == null.");
-            } else {
-                Log.d(this, "stopping local tone.");
-                mToneGenerator.stopTone();
-            }
-        }
+
+        getHandler().sendMessage(
+                getHandler().obtainMessage(EVENT_STOP_TONE, Log.createSubsession()));
     }
 
     /**
@@ -125,7 +200,8 @@
 
         if (areLocalTonesEnabled) {
             Log.d(this, "Posting create.");
-            postMessage(EVENT_CREATE_OBJECT);
+            getHandler().sendMessage(
+                    getHandler().obtainMessage(EVENT_START_SESSION, Log.createSubsession()));
         }
     }
 
@@ -141,75 +217,23 @@
 
             mCall = null;
             Log.d(this, "Posting delete.");
-            postMessage(EVENT_DELETE_OBJECT);
+            getHandler().sendMessage(
+                    getHandler().obtainMessage(EVENT_END_SESSION, Log.createSubsession()));
         }
     }
 
     /**
-     * Posts a message to the tonegenerator-thread handler. Creates the handler if the handler
-     * has not been instantiated.
-     *
-     * @param messageCode The message to post.
+     * Creates a new ToneHandler on a separate thread if none exists, and returns it.
+     * No need for locking, since everything that calls this is protected by the Telecom lock.
      */
-    private void postMessage(int messageCode) {
-        synchronized(this) {
-            if (mHandler == null) {
-                mHandler = getNewHandler();
-            }
-
-            if (mHandler == null) {
-                Log.d(this, "Message %d skipped because there is no handler.", messageCode);
-            } else {
-                mHandler.obtainMessage(messageCode, null).sendToTarget();
-            }
+    @VisibleForTesting
+    public ToneHandler getHandler() {
+        if (mHandler == null) {
+            HandlerThread thread = new HandlerThread("tonegenerator-dtmf");
+            thread.start();
+            mHandler = new ToneHandler(thread.getLooper());
         }
-    }
-
-    /**
-     * Creates a new tonegenerator Handler running in its own thread.
-     */
-    private Handler getNewHandler() {
-        Preconditions.checkState(mHandler == null);
-
-        HandlerThread thread = new HandlerThread("tonegenerator-dtmf");
-        thread.start();
-
-        return new Handler(thread.getLooper()) {
-            @Override
-            public void handleMessage(Message msg) {
-                switch(msg.what) {
-                    case EVENT_CREATE_OBJECT:
-                        synchronized(DtmfLocalTonePlayer.this) {
-                            if (mToneGenerator == null) {
-                                try {
-                                    mToneGenerator = new ToneGenerator(
-                                            AudioManager.STREAM_DTMF, 80);
-                                } catch (RuntimeException e) {
-                                    Log.e(this, e, "Error creating local tone generator.");
-                                    mToneGenerator = null;
-                                }
-                            }
-                        }
-                        break;
-                    case EVENT_DELETE_OBJECT:
-                        synchronized(DtmfLocalTonePlayer.this) {
-                            if (mToneGenerator != null) {
-                                mToneGenerator.release();
-                                mToneGenerator = null;
-                            }
-                            // Delete the handler after the tone generator object is deleted by
-                            // the tonegenerator thread.
-                            if (mHandler != null && !mHandler.hasMessages(EVENT_CREATE_OBJECT)) {
-                                // Stop the handler only if there are no pending CREATE messages.
-                                mHandler.removeMessages(EVENT_DELETE_OBJECT);
-                                mHandler.getLooper().quitSafely();
-                                mHandler = null;
-                            }
-                        }
-                        break;
-                }
-            }
-        };
+        return mHandler;
     }
 
     private static int getMappedTone(char digit) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 4b54760..0eeec44 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -496,13 +496,18 @@
     }
 
     @Override
-    public void sendRttRequest() {
+    public void sendRttRequest(String callId) {
         try {
             Log.startSession("ICA.sRR");
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    // TODO
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.sendRttRequest();
+                    } else {
+                        Log.w(this, "stopRtt(): call %s not found", callId);
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -513,13 +518,18 @@
     }
 
     @Override
-    public void respondToRttRequest(int id, boolean accept) {
+    public void respondToRttRequest(String callId, int id, boolean accept) {
         try {
             Log.startSession("ICA.rTRR");
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    // TODO
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.handleRttRequestResponse(id, accept);
+                    } else {
+                        Log.w(this, "respondToRttRequest(): call %s not found", callId);
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -530,13 +540,18 @@
     }
 
     @Override
-    public void stopRtt() {
+    public void stopRtt(String callId) {
         try {
             Log.startSession("ICA.sRTT");
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    // TODO
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.stopRtt();
+                    } else {
+                        Log.w(this, "stopRtt(): call %s not found", callId);
+                    }
                 }
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -547,7 +562,7 @@
     }
 
     @Override
-    public void setRttMode(int mode) {
+    public void setRttMode(String callId, int mode) {
         try {
             Log.startSession("ICA.sRM");
             long token = Binder.clearCallingIdentity();
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index dce4364..e575097 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -29,7 +29,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Parcel;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -84,13 +83,16 @@
     private class InCallServiceInfo {
         private final ComponentName mComponentName;
         private boolean mIsExternalCallsSupported;
+        private boolean mIsSelfManagedCallsSupported;
         private final int mType;
 
         public InCallServiceInfo(ComponentName componentName,
                 boolean isExternalCallsSupported,
+                boolean isSelfManageCallsSupported,
                 int type) {
             mComponentName = componentName;
             mIsExternalCallsSupported = isExternalCallsSupported;
+            mIsSelfManagedCallsSupported = isSelfManageCallsSupported;
             mType = type;
         }
 
@@ -102,6 +104,10 @@
             return mIsExternalCallsSupported;
         }
 
+        public boolean isSelfManagedCallsSupported() {
+            return mIsSelfManagedCallsSupported;
+        }
+
         public int getType() {
             return mType;
         }
@@ -120,18 +126,23 @@
             if (mIsExternalCallsSupported != that.mIsExternalCallsSupported) {
                 return false;
             }
+            if (mIsSelfManagedCallsSupported != that.mIsSelfManagedCallsSupported) {
+                return false;
+            }
             return mComponentName.equals(that.mComponentName);
 
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mComponentName, mIsExternalCallsSupported);
+            return Objects.hash(mComponentName, mIsExternalCallsSupported,
+                    mIsSelfManagedCallsSupported);
         }
 
         @Override
         public String toString() {
-            return "[" + mComponentName + " supportsExternal? " + mIsExternalCallsSupported + "]";
+            return "[" + mComponentName + " supportsExternal? " + mIsExternalCallsSupported +
+                    " supportsSelfMg?" + mIsSelfManagedCallsSupported + "]";
         }
     }
 
@@ -599,6 +610,17 @@
         public void onConnectionEvent(Call call, String event, Bundle extras) {
             notifyConnectionEvent(call, event, extras);
         }
+
+        @Override
+        public void onRttInitiationFailure(Call call, int reason) {
+            notifyRttInitiationFailure(call, reason);
+            updateCall(call, false, true);
+        }
+
+        @Override
+        public void onRemoteRttRequest(Call call, int requestId) {
+            notifyRemoteRttRequest(call, requestId);
+        }
     };
 
     private final SystemStateListener mSystemStateListener = new SystemStateListener() {
@@ -658,11 +680,6 @@
 
     @Override
     public void onCallAdded(Call call) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onCallAdded: skipped self-managed %s", call);
-            return;
-        }
-
         if (!isBoundToServices()) {
             bindToServices(call);
         } else {
@@ -680,6 +697,10 @@
                     continue;
                 }
 
+                if (call.isSelfManaged() && !info.isSelfManagedCallsSupported()) {
+                    continue;
+                }
+
                 // Only send the RTT call if it's a UI in-call service
                 boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
 
@@ -700,11 +721,6 @@
 
     @Override
     public void onCallRemoved(Call call) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onCallRemoved: skipped self-managed %s", call);
-            return;
-        }
-
         Log.i(this, "onCallRemoved: %s", call);
         if (mCallsManager.getCalls().isEmpty()) {
             /** Let's add a 2 second delay before we send unbind to the services to hopefully
@@ -728,11 +744,6 @@
 
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
-            return;
-        }
-
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
 
         List<ComponentName> componentsUpdated = new ArrayList<>();
@@ -748,6 +759,10 @@
                     continue;
                 }
 
+                if (call.isSelfManaged() && !info.isSelfManagedCallsSupported()) {
+                    continue;
+                }
+
                 componentsUpdated.add(info.getComponentName());
                 IInCallService inCallService = entry.getValue();
 
@@ -798,11 +813,6 @@
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
-            return;
-        }
-
         updateCall(call);
     }
 
@@ -811,11 +821,6 @@
             Call call,
             ConnectionServiceWrapper oldService,
             ConnectionServiceWrapper newService) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onConnectionServiceChanged: skipped self-managed %s", call);
-            return;
-        }
-
         updateCall(call);
     }
 
@@ -861,11 +866,6 @@
 
     @Override
     public void onIsConferencedChanged(Call call) {
-        if (call.isSelfManaged()) {
-            Log.i(this, "onIsConferencedChanged: skipped self-managed %s", call);
-            return;
-        }
-
         Log.d(this, "onIsConferencedChanged %s", call);
         updateCall(call);
     }
@@ -909,6 +909,37 @@
         }
     }
 
+    private void notifyRttInitiationFailure(Call call, int reason) {
+        if (!mInCallServices.isEmpty()) {
+             mInCallServices.entrySet().stream()
+                    .filter((entry) -> entry.getKey().equals(mInCallServiceConnection.getInfo()))
+                    .forEach((entry) -> {
+                        try {
+                            Log.i(this, "notifyRttFailure, call %s, incall %s",
+                                    call, entry.getKey());
+                            entry.getValue().onRttInitiationFailure(mCallIdMapper.getCallId(call),
+                                    reason);
+                        } catch (RemoteException ignored) {
+                        }
+                    });
+        }
+    }
+
+    private void notifyRemoteRttRequest(Call call, int requestId) {
+        if (!mInCallServices.isEmpty()) {
+            mInCallServices.entrySet().stream()
+                    .filter((entry) -> entry.getKey().equals(mInCallServiceConnection.getInfo()))
+                    .forEach((entry) -> {
+                        try {
+                            Log.i(this, "notifyRemoteRttRequest, call %s, incall %s",
+                                    call, entry.getKey());
+                            entry.getValue().onRttUpgradeRequest(
+                                    mCallIdMapper.getCallId(call), requestId);
+                        } catch (RemoteException ignored) {
+                        }
+                    });
+        }
+    }
     /**
      * Unbinds an existing bound connection to the in-call app.
      */
@@ -992,7 +1023,7 @@
             // 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);
+            return new InCallServiceInfo(componentName, false, false, type);
         }
     }
 
@@ -1041,12 +1072,15 @@
                 boolean isExternalCallsSupported = serviceInfo.metaData != null &&
                         serviceInfo.metaData.getBoolean(
                                 TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, false);
+                boolean isSelfManageCallsSupported = serviceInfo.metaData != null &&
+                        serviceInfo.metaData.getBoolean(
+                                TelecomManager.METADATA_INCLUDE_SELF_MANAGED_CALLS, false);
                 if (requestedType == 0 || requestedType == getInCallServiceType(entry.serviceInfo,
                         packageManager)) {
 
                     retval.add(new InCallServiceInfo(
                             new ComponentName(serviceInfo.packageName, serviceInfo.name),
-                            isExternalCallsSupported, requestedType));
+                            isExternalCallsSupported, isSelfManageCallsSupported, requestedType));
                 }
             }
         }
@@ -1160,7 +1194,7 @@
         int numCallsSent = 0;
         for (Call call : calls) {
             try {
-                if (call.isSelfManaged() ||
+                if ((call.isSelfManaged() && !info.isSelfManagedCallsSupported()) ||
                         (call.isExternalCall() && !info.isExternalCallsSupported())) {
                     continue;
                 }
@@ -1229,6 +1263,10 @@
                     continue;
                 }
 
+                if (call.isSelfManaged() && !info.isSelfManagedCallsSupported()) {
+                    continue;
+                }
+
                 ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
                         call,
                         videoProviderChanged /* includeVideoProvider */,
diff --git a/src/com/android/server/telecom/InterruptionFilterProxy.java b/src/com/android/server/telecom/InterruptionFilterProxy.java
index 434c341..b659de8 100644
--- a/src/com/android/server/telecom/InterruptionFilterProxy.java
+++ b/src/com/android/server/telecom/InterruptionFilterProxy.java
@@ -24,4 +24,5 @@
 public interface InterruptionFilterProxy {
     void setInterruptionFilter(int interruptionFilter);
     int getCurrentInterruptionFilter();
+    String getInterruptionModeInitiator();
 }
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index e855549..cbe3676 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -332,7 +332,10 @@
         android.telecom.Call.Details.PROPERTY_IS_EXTERNAL_CALL,
 
         Connection.PROPERTY_HAS_CDMA_VOICE_PRIVACY,
-        android.telecom.Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY
+        android.telecom.Call.Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY,
+
+        Connection.PROPERTY_SELF_MANAGED,
+        android.telecom.Call.Details.PROPERTY_SELF_MANAGED
     };
 
     private static int convertConnectionToCallProperties(int connectionProperties) {
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 51f11d7..74eb8bc 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -648,6 +648,26 @@
         }
 
         /**
+         * @see android.telecom.TelecomManager#isInManagedCall
+         */
+        @Override
+        public boolean isInManagedCall(String callingPackage) {
+            try {
+                Log.startSession("TSI.iIMC");
+                if (!canReadPhoneState(callingPackage, "isInManagedCall")) {
+                    throw new SecurityException("Only the default dialer or caller with " +
+                            "READ_PHONE_STATE permission can use this method.");
+                }
+
+                synchronized (mLock) {
+                    return mCallsManager.hasOngoingManagedCalls();
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        /**
          * @see android.telecom.TelecomManager#isRinging
          */
         @Override
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 50b8901..a76821a 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -94,8 +94,8 @@
                 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;
+                    retval = (SubscriptionManager.getSlotIndex(subId1) <
+                            SubscriptionManager.getSlotIndex(subId2)) ? -1 : 1;
                 }
 
                 // Then order by package
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index 480d8c9..62613b0 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -303,10 +304,12 @@
      * @param callingPackage The package calling in.
      * @param callingUid The UID of the caller.
      * @param callingPid The PID of the caller.
+     * @param targetSdkVersion The target SDK version of the calling InCallService where the camera
+     *      request originated.
      */
     @Override
     public void onSetCamera(String cameraId, String callingPackage, int callingUid,
-            int callingPid) {
+            int callingPid, int targetSdkVersion) {
         synchronized (mLock) {
             logFromInCall("setCamera: " + cameraId + " callingPackage=" + callingPackage +
                     "; callingUid=" + callingUid);
@@ -315,16 +318,25 @@
                 if (!canUseCamera(mCall.getContext(), callingPackage, callingUid, callingPid)) {
                     // Calling app is not permitted to use the camera.  Ignore the request and send
                     // back a call session event indicating the error.
-                    Log.i(this, "onSetCamera: camera permission denied; package=%d, uid=%d, "
-                            + "pid=%d",
-                            callingPackage, callingUid, callingPid);
-                    VideoProviderProxy.this.handleCallSessionEvent(
-                            Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR);
+                    Log.i(this, "onSetCamera: camera permission denied; package=%s, uid=%d, "
+                            + "pid=%d, targetSdkVersion=%d",
+                            callingPackage, callingUid, callingPid, targetSdkVersion);
+
+                    // API 26 introduces a new camera permission error we can use here since the
+                    // caller supports that API version.
+                    if (targetSdkVersion > Build.VERSION_CODES.N_MR1) {
+                        VideoProviderProxy.this.handleCallSessionEvent(
+                                Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR);
+                    } else {
+                        VideoProviderProxy.this.handleCallSessionEvent(
+                                Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
+                    }
                     return;
                 }
             }
             try {
-                mConectionServiceVideoProvider.setCamera(cameraId, callingPackage);
+                mConectionServiceVideoProvider.setCamera(cameraId, callingPackage,
+                        targetSdkVersion);
             } catch (RemoteException e) {
                 VideoProviderProxy.this.handleCallSessionEvent(
                         Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index ea1db36..a066b6c 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -26,6 +26,7 @@
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.ServiceManager;
+import android.service.notification.ZenModeConfig;
 import android.telecom.Log;
 
 import com.android.internal.telephony.CallerInfoAsyncQuery;
@@ -170,6 +171,15 @@
                                 public int getCurrentInterruptionFilter() {
                                     return notificationManager.getCurrentInterruptionFilter();
                                 }
+
+                                @Override
+                                public String getInterruptionModeInitiator() {
+                                    ZenModeConfig config = notificationManager.getZenModeConfig();
+                                    if (config.manualRule != null) {
+                                        return config.manualRule.enabler;
+                                    }
+                                    return null;
+                                }
                             }
                     ));
         }
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index a6a0d76..cc6470e 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -120,6 +120,10 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:scheme="tel" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="android.telecom.testapps.ACTION_REMOTE_RTT_UPGRADE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
 
         <receiver android:name="com.android.server.telecom.testapps.CallNotificationReceiver"
@@ -173,6 +177,14 @@
           </intent-filter>
         </activity>
 
+        <activity android:name="com.android.server.telecom.testapps.IncomingSelfManagedCallActivity"
+                  android:label="@string/selfManagedCallingActivityLabel"
+                  android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+          <intent-filter>
+              <action android:name="android.intent.action.MAIN" />
+          </intent-filter>
+        </activity>
+
         <service android:name="com.android.server.telecom.testapps.SelfManagedConnectionService"
                  android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
                  android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
@@ -180,5 +192,9 @@
               <action android:name="android.telecom.ConnectionService" />
           </intent-filter>
         </service>
+
+        <receiver android:exported="false"
+            android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
+            android:name="com.android.server.telecom.testapps.SelfManagedCallNotificationReceiver" />
     </application>
 </manifest>
diff --git a/testapps/res/drawable-hdpi/ic_android_black_24dp.png b/testapps/res/drawable-hdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..ed3ee45
--- /dev/null
+++ b/testapps/res/drawable-hdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/res/drawable-mdpi/ic_android_black_24dp.png b/testapps/res/drawable-mdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..a4add51
--- /dev/null
+++ b/testapps/res/drawable-mdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/res/drawable-xhdpi/ic_android_black_24dp.png b/testapps/res/drawable-xhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..41558f2
--- /dev/null
+++ b/testapps/res/drawable-xhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/res/drawable-xxhdpi/ic_android_black_24dp.png b/testapps/res/drawable-xxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..6006b12
--- /dev/null
+++ b/testapps/res/drawable-xxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/res/drawable-xxxhdpi/ic_android_black_24dp.png b/testapps/res/drawable-xxxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..4f935bf
--- /dev/null
+++ b/testapps/res/drawable-xxxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/res/layout/incall_screen.xml b/testapps/res/layout/incall_screen.xml
index 3ca8781..502bdf4 100644
--- a/testapps/res/layout/incall_screen.xml
+++ b/testapps/res/layout/incall_screen.xml
@@ -26,7 +26,8 @@
             android:divider="#FFCC00"
             android:dividerHeight="4px">
     </ListView>
-    <LinearLayout
+    <GridLayout
+        android:columnCount="4"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal">
@@ -55,5 +56,15 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/answerCallButton"/>
-    </LinearLayout>
+        <Button
+            android:id="@+id/start_rtt_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/startRttButton"/>
+        <Button
+            android:id="@+id/accept_rtt_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/acceptRttButton"/>
+    </GridLayout>
 </LinearLayout>
diff --git a/testapps/res/layout/self_managed_incoming_call.xml b/testapps/res/layout/self_managed_incoming_call.xml
new file mode 100644
index 0000000..c346342
--- /dev/null
+++ b/testapps/res/layout/self_managed_incoming_call.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:text="Incoming Call!"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/incomingCallText" />
+
+    <Button
+        android:text="Answer Call"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/answerCallButton" />
+
+    <Button
+        android:text="Reject Call"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/rejectCallButton" />
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
index f239997..144d6ec 100644
--- a/testapps/res/layout/self_managed_sample_main.xml
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -68,6 +68,11 @@
                 android:layout_height="wrap_content"
                 android:text="Incoming Call"
                 android:id="@+id/placeIncomingCallButton" />
+            <Button
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Incoming Call (Delay)"
+                android:id="@+id/placeIncomingCallDelayButton" />
         </TableRow>
         <TableRow android:layout_span="2">
             <ListView
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 56cb476..9dc7cae 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -48,6 +48,10 @@
 
     <string name="endRttButton">End RTT</string>
 
+    <string name="startRttButton">Start RTT</string>
+
+    <string name="acceptRttButton">Accept RTT request</string>
+
     <string name="muteButton">Mute</string>
 
     <string name="holdButton">Hold</string>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
index 8fd2378..2d43e76 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -144,4 +144,9 @@
         intent.setData(data);
         LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
     }
+
+    public static void remoteRttUpgrade(Context context) {
+        final Intent intent = new Intent(TestCallActivity.ACTION_REMOTE_RTT_UPGRADE);
+        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+    }
 }
diff --git a/testapps/src/com/android/server/telecom/testapps/IncomingSelfManagedCallActivity.java b/testapps/src/com/android/server/telecom/testapps/IncomingSelfManagedCallActivity.java
new file mode 100644
index 0000000..05b6a6b
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/IncomingSelfManagedCallActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telecom.Log;
+import android.telephony.DisconnectCause;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.server.telecom.testapps.R;
+
+/**
+ * Sample Incoming Call activity for Self-Managed calls.
+ */
+public class IncomingSelfManagedCallActivity extends Activity {
+    public static final String EXTRA_CALL_ID = "com.android.server.telecom.testapps.extra.CALL_ID";
+
+    private Button mAnswerCallButton;
+    private Button mRejectCallButton;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent launchingIntent = getIntent();
+        int callId = launchingIntent.getIntExtra(EXTRA_CALL_ID, 0);
+        Log.i(this, "showing fullscreen answer ux for call id %d", callId);
+
+        setContentView(R.layout.self_managed_incoming_call);
+        final SelfManagedConnection connection = SelfManagedCallList.getInstance()
+                .getConnectionById(callId);
+        mAnswerCallButton = (Button) findViewById(R.id.answerCallButton);
+        mAnswerCallButton.setOnClickListener((View v) -> {
+            if (connection != null) {
+                connection.setConnectionActive();
+            }
+            finish();
+        });
+        mRejectCallButton = (Button) findViewById(R.id.rejectCallButton);
+        mRejectCallButton.setOnClickListener((View v) -> {
+            if (connection != null) {
+                connection.setConnectionDisconnected(DisconnectCause.INCOMING_REJECTED);
+                connection.destroy();
+            }
+            finish();
+        });
+    }
+}
\ No newline at end of file
diff --git a/testapps/src/com/android/server/telecom/testapps/RttChatbot.java b/testapps/src/com/android/server/telecom/testapps/RttChatbot.java
index 3b16bd4..44439ee 100644
--- a/testapps/src/com/android/server/telecom/testapps/RttChatbot.java
+++ b/testapps/src/com/android/server/telecom/testapps/RttChatbot.java
@@ -47,6 +47,8 @@
     private final Random mRandom = new Random();
     private final String[] mOneLiners;
     private Handler mHandler;
+    private HandlerThread mSenderThread;
+    private Thread mReceiverThread;
 
     private final class ReplyHandler extends Handler {
         private StringBuilder mInputSoFar;
@@ -110,8 +112,9 @@
         Log.i(LOG_TAG, "Starting RTT chatbot.");
         HandlerThread ht = new HandlerThread("RttChatbotSender");
         ht.start();
+        mSenderThread = ht;
         mHandler = new ReplyHandler(ht.getLooper());
-        Thread receiveThread = new Thread(() -> {
+        mReceiverThread = new Thread(() -> {
             while (true) {
                 String charsReceived = mRttTextStream.read();
                 if (charsReceived == null) {
@@ -129,6 +132,15 @@
                         .sendToTarget();
             }
         }, "RttChatbotReceiver");
-        receiveThread.start();
+        mReceiverThread.start();
+    }
+
+    public void stop() {
+        if (mSenderThread != null && mSenderThread.isAlive()) {
+            mSenderThread.quit();
+        }
+        if (mReceiverThread != null && mReceiverThread.isAlive()) {
+            mReceiverThread.interrupt();
+        }
     }
 }
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index e0d7442..1d6367e 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -29,6 +29,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Manages the list of {@link SelfManagedConnection} active in the sample third-party calling app.
@@ -64,6 +65,25 @@
 
     private List<SelfManagedConnection> mConnections = new ArrayList<>();
 
+    private SelfManagedConnection.Listener mConnectionListener =
+            new SelfManagedConnection.Listener() {
+                @Override
+                public void onConnectionStateChanged(SelfManagedConnection connection) {
+                    notifyCallModified();
+                }
+
+                @Override
+                public void onConnectionRemoved(SelfManagedConnection connection) {
+                    removeConnection(connection);
+                    notifyCallModified();
+                }
+    };
+
+    public SelfManagedConnection.Listener getConnectionListener() {
+        return mConnectionListener;
+    }
+
+
     public void setListener(Listener listener) {
         mListener = listener;
     }
@@ -125,6 +145,13 @@
         return mConnections;
     }
 
+    public SelfManagedConnection getConnectionById(int callId) {
+        Optional<SelfManagedConnection> foundOptional = mConnections.stream()
+                .filter((c) -> {return c.getCallId() == callId;})
+                .findFirst();
+        return foundOptional.orElse(null);
+    }
+
     public void notifyCallModified() {
         if (mListener != null) {
             mListener.onConnectionListChanged();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallNotificationReceiver.java
new file mode 100644
index 0000000..81a9bfa
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallNotificationReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.Log;
+import android.telephony.DisconnectCause;
+
+/**
+ * Handles actions from the self-managed calling sample app incoming call UX.
+ */
+public class SelfManagedCallNotificationReceiver extends BroadcastReceiver {
+    public static final String ACTION_ANSWER_CALL =
+            "com.android.server.telecom.testapps.action.ANSWER_CALL";
+    public static final String ACTION_REJECT_CALL =
+            "com.android.server.telecom.testapps.action.REJECT_CALL";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        int callId = intent.getIntExtra(IncomingSelfManagedCallActivity.EXTRA_CALL_ID, 0);
+        NotificationManager notificationManager = context.getSystemService(
+                NotificationManager.class);
+        SelfManagedConnection connection = SelfManagedCallList.getInstance()
+                .getConnectionById(callId);
+        switch (action) {
+            case ACTION_ANSWER_CALL:
+                Log.i(this, "onReceive - answerCall %d", callId);
+                if (connection != null) {
+                    connection.setConnectionActive();
+                }
+                notificationManager.cancel(SelfManagedConnection.CALL_NOTIFICATION, callId);
+                break;
+
+            case ACTION_REJECT_CALL:
+                Log.i(this, "onReceive - rejectCall %d", callId);
+                if (connection != null) {
+                    connection.setConnectionDisconnected(DisconnectCause.INCOMING_REJECTED);
+                    connection.destroy();
+                }
+                notificationManager.cancel(SelfManagedConnection.CALL_NOTIFICATION, callId);
+                break;
+        }
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 8b7eae0..c8f6157 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -24,6 +24,7 @@
 import android.telecom.TelecomManager;
 import android.util.Log;
 import android.view.View;
+import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
@@ -45,6 +46,7 @@
     private CheckBox mCheckIfPermittedBeforeCalling;
     private Button mPlaceOutgoingCallButton;
     private Button mPlaceIncomingCallButton;
+    private Button mPlaceIncomingCallDelayButton;
     private RadioButton mUseAcct1Button;
     private RadioButton mUseAcct2Button;
     private EditText mNumber;
@@ -76,6 +78,12 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        int flags =
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                        | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                        | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+
+        getWindow().addFlags(flags);
         setContentView(R.layout.self_managed_sample_main);
         mCheckIfPermittedBeforeCalling = (CheckBox) findViewById(
                 R.id.checkIfPermittedBeforeCalling);
@@ -93,6 +101,20 @@
                 placeIncomingCall();
             }
         });
+
+        mPlaceIncomingCallDelayButton = (Button) findViewById(R.id.placeIncomingCallDelayButton);
+        mPlaceIncomingCallDelayButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Delay the incoming call so that we can turn off the screen and
+                try {
+                    Thread.sleep(5000);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                placeIncomingCall();
+            }
+        });
         mUseAcct1Button = (RadioButton) findViewById(R.id.useAcct1Button);
         mUseAcct2Button = (RadioButton) findViewById(R.id.useAcct2Button);
         mNumber = (EditText) findViewById(R.id.phoneNumber);
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
index 5fd8eba..051948b 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
@@ -16,31 +16,55 @@
 
 package com.android.server.telecom.testapps;
 
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
 import android.media.MediaPlayer;
 import android.telecom.CallAudioState;
 import android.telecom.Connection;
 import android.telecom.ConnectionService;
 import android.telecom.DisconnectCause;
 
+import com.android.server.telecom.testapps.R;
+
 /**
  * Sample self-managed {@link Connection} for a self-managed {@link ConnectionService}.
  * <p>
  * See {@link android.telecom} for more information on self-managed {@link ConnectionService}s.
  */
 public class SelfManagedConnection extends Connection {
+    public static class Listener {
+        public void onConnectionStateChanged(SelfManagedConnection connection) {}
+        public void onConnectionRemoved(SelfManagedConnection connection) {}
+    }
+
     public static final String EXTRA_PHONE_ACCOUNT_HANDLE =
             "com.android.server.telecom.testapps.extra.PHONE_ACCOUNT_HANDLE";
+    public static final String CALL_NOTIFICATION = "com.android.server.telecom.testapps.CALL";
 
+    private static int sNextCallId = 1;
+
+    private final int mCallId;
+    private final Context mContext;
     private final SelfManagedCallList mCallList;
     private final MediaPlayer mMediaPlayer;
     private final boolean mIsIncomingCall;
     private boolean mIsIncomingCallUiShowing;
+    private Listener mListener;
 
     SelfManagedConnection(SelfManagedCallList callList, Context context, boolean isIncoming) {
         mCallList = callList;
         mMediaPlayer = createMediaPlayer(context);
         mIsIncomingCall = isIncoming;
+        mContext = context;
+        mCallId = sNextCallId++;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
     }
 
     /**
@@ -52,20 +76,82 @@
         mCallList.notifyCallModified();
     }
 
+    @Override
+    public void onShowIncomingCallUi() {
+        // Create the fullscreen intent used to show the fullscreen incoming call UX.
+        Intent intent = new Intent(Intent.ACTION_MAIN, null);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setClass(mContext, IncomingSelfManagedCallActivity.class);
+        intent.putExtra(IncomingSelfManagedCallActivity.EXTRA_CALL_ID, mCallId);
+        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 1, intent, 0);
+
+        // Build the notification as an ongoing high priority item.
+        final Notification.Builder builder = new Notification.Builder(mContext);
+        builder.setOngoing(true);
+        builder.setPriority(Notification.PRIORITY_HIGH);
+
+        // Set up the main intent to send the user to the incoming call screen.
+        builder.setContentIntent(pendingIntent);
+        builder.setFullScreenIntent(pendingIntent, true);
+
+        // Setup notification content.
+        builder.setSmallIcon(R.drawable.ic_android_black_24dp);
+        builder.setContentTitle("Incoming call...");
+        builder.setContentText("Incoming test call from " + getAddress());
+
+        // Setup answer and reject call button
+        final Intent answerIntent = new Intent(
+                SelfManagedCallNotificationReceiver.ACTION_ANSWER_CALL, null, mContext,
+                SelfManagedCallNotificationReceiver.class);
+        answerIntent.putExtra(IncomingSelfManagedCallActivity.EXTRA_CALL_ID, mCallId);
+        final Intent rejectIntent = new Intent(
+                SelfManagedCallNotificationReceiver.ACTION_REJECT_CALL, null, mContext,
+                SelfManagedCallNotificationReceiver.class);
+        rejectIntent.putExtra(IncomingSelfManagedCallActivity.EXTRA_CALL_ID, mCallId);
+
+        builder.addAction(
+                new Notification.Action.Builder(
+                        Icon.createWithResource(mContext, R.drawable.ic_android_black_24dp),
+                        "Answer",
+                        PendingIntent.getBroadcast(mContext, 0, answerIntent,
+                                PendingIntent.FLAG_UPDATE_CURRENT))
+                        .build());
+        builder.addAction(
+                new Notification.Action.Builder(
+                        Icon.createWithResource(mContext, R.drawable.ic_android_black_24dp),
+                        "Reject",
+                        PendingIntent.getBroadcast(mContext, 0, rejectIntent,
+                                PendingIntent.FLAG_UPDATE_CURRENT))
+                        .build());
+
+        NotificationManager notificationManager = mContext.getSystemService(
+                NotificationManager.class);
+        notificationManager.notify(CALL_NOTIFICATION, mCallId, builder.build());
+    }
+
     public void setConnectionActive() {
         mMediaPlayer.start();
         setActive();
+        if (mListener != null ) {
+            mListener.onConnectionStateChanged(this);
+        }
     }
 
     public void setConnectionHeld() {
         mMediaPlayer.pause();
         setOnHold();
+        if (mListener != null ) {
+            mListener.onConnectionStateChanged(this);
+        }
     }
 
     public void setConnectionDisconnected(int cause) {
         mMediaPlayer.stop();
         setDisconnected(new DisconnectCause(cause));
         destroy();
+        if (mListener != null ) {
+            mListener.onConnectionRemoved(this);
+        }
     }
 
     public void setIsIncomingCallUiShowing(boolean showing) {
@@ -80,6 +166,10 @@
         return mIsIncomingCall;
     }
 
+    public int getCallId() {
+        return mCallId;
+    }
+
     private MediaPlayer createMediaPlayer(Context context) {
         int audioToPlay = (Math.random() > 0.5f) ? R.raw.sample_audio : R.raw.sample_audio2;
         MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
index 31bbcd6..e1a01ba 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -49,12 +49,14 @@
     }
 
     @Override
-    public void onCreateIncomingConnectionFailed(ConnectionRequest request) {
+    public void onCreateIncomingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount,
+                                                 ConnectionRequest request) {
         mCallList.notifyCreateIncomingConnectionFailed(request);
     }
 
     @Override
-    public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {
+    public void onCreateOutgoingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount,
+                                                 ConnectionRequest request) {
         mCallList.notifyCreateOutgoingConnectionFailed(request);
     }
 
@@ -62,6 +64,7 @@
     private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming) {
         SelfManagedConnection connection = new SelfManagedConnection(mCallList,
                 getApplicationContext(), isIncoming);
+        connection.setListener(mCallList.getConnectionListener());
         connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
         connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
         connection.setExtras(request.getExtras());
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
index 76f2058..ae606c8 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
@@ -55,6 +55,9 @@
     static final String ACTION_RTT_CALL =
             "android.telecom.testapps.ACTION_RTT_CALL";
 
+    public static final String ACTION_REMOTE_RTT_UPGRADE =
+            "android.telecom.testapps.ACTION_REMOTE_RTT_UPGRADE";
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -73,6 +76,8 @@
                     this, data, VideoProfile.STATE_AUDIO_ONLY);
         } else if (ACTION_SEND_UPGRADE_REQUEST.equals(action)) {
             CallNotificationReceiver.sendUpgradeRequest(this, data);
+        } else if (ACTION_REMOTE_RTT_UPGRADE.equals(action)) {
+            CallNotificationReceiver.remoteRttUpgrade(this);
         } else {
             CallServiceNotifier.getInstance().updateNotification(this);
         }
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallList.java b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
index 4419b17..1b32690 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.testapps;
 
+import android.content.Context;
 import android.telecom.Call;
 import android.telecom.InCallService;
 import android.telecom.VideoProfile;
@@ -23,6 +24,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.widget.Toast;
 
 import java.util.LinkedList;
 import java.util.List;
@@ -37,6 +39,10 @@
     public static abstract class Listener {
         public void onCallAdded(Call call) {}
         public void onCallRemoved(Call call) {}
+        public void onRttStarted(Call call) {}
+        public void onRttStopped(Call call) {}
+        public void onRttInitiationFailed(Call call, int reason) {}
+        public void onRttRequest(Call call, int id) {}
     }
 
     private static final TestCallList INSTANCE = new TestCallList();
@@ -97,6 +103,8 @@
     private Map<Call, TestVideoCallListener> mVideoCallListeners =
             new ArrayMap<Call, TestVideoCallListener>();
     private Set<Listener> mListeners = new ArraySet<Listener>();
+    private Context mContext;
+    private int mLastRttRequestId = -1;
 
     /**
      * Singleton accessor.
@@ -164,6 +172,10 @@
         return mCalls.size();
     }
 
+    public int getLastRttRequestId() {
+        return mLastRttRequestId;
+    }
+
     /**
      * For any video calls tracked, sends an upgrade to video request.
      */
@@ -218,11 +230,29 @@
     @Override
     public void onRttStatusChanged(Call call, boolean enabled, Call.RttCall rttCall) {
         Log.v(TAG, "onRttStatusChanged: call = " + call + " " + System.identityHashCode(this));
+        if (enabled) {
+            for (Listener l : mListeners) {
+                l.onRttStarted(call);
+            }
+        } else {
+            for (Listener l : mListeners) {
+                l.onRttStopped(call);
+            }
+        }
+    }
 
-        if (call != null) {
-            // Did you have another call? Well too bad, this class isn't gonna handle it.
-            mCalls.clear();
-            mCalls.add(call);
+    @Override
+    public void onRttInitiationFailure(Call call, int reason) {
+        for (Listener l : mListeners) {
+            l.onRttInitiationFailed(call, reason);
+        }
+    }
+
+    @Override
+    public void onRttRequest(Call call, int id) {
+        mLastRttRequestId = id;
+        for (Listener l : mListeners) {
+            l.onRttRequest(call, id);
         }
     }
 }
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
index c2d8852..abb9108 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
@@ -124,6 +124,26 @@
                 }
                 setConferenceableConnections(c);
             }
+
+            @Override
+            public void onRttInitiationSuccess(RemoteConnection connection) {
+                sendRttInitiationSuccess();
+            }
+
+            @Override
+            public void onRttInitiationFailure(RemoteConnection connection, int reason) {
+                sendRttInitiationFailure(reason);
+            }
+
+            @Override
+            public void onRttSessionRemotelyTerminated(RemoteConnection connection) {
+                sendRttSessionRemotelyTerminated();
+            }
+
+            @Override
+            public void onRemoteRttRequest(RemoteConnection connection) {
+                sendRemoteRttRequest();
+            }
         };
 
         private final RemoteConnection mRemote;
@@ -143,13 +163,17 @@
             mRemote.abort();
         }
 
-        /** ${inheritDoc} */
+        /**
+         * ${inheritDoc}
+         */
         @Override
         public void onAnswer(int videoState) {
             mRemote.answer(videoState);
         }
 
-        /** ${inheritDoc} */
+        /**
+         * ${inheritDoc}
+         */
         @Override
         public void onDisconnect() {
             mRemote.disconnect();
@@ -160,19 +184,25 @@
             mRemote.playDtmfTone(c);
         }
 
-        /** ${inheritDoc} */
+        /**
+         * ${inheritDoc}
+         */
         @Override
         public void onHold() {
             mRemote.hold();
         }
 
-        /** ${inheritDoc} */
+        /**
+         * ${inheritDoc}
+         */
         @Override
         public void onReject() {
             mRemote.reject();
         }
 
-        /** ${inheritDoc} */
+        /**
+         * ${inheritDoc}
+         */
         @Override
         public void onUnhold() {
             mRemote.unhold();
@@ -183,6 +213,21 @@
             mRemote.setCallAudioState(state);
         }
 
+        @Override
+        public void onStartRtt(RttTextStream rttTextStream) {
+            mRemote.startRtt(rttTextStream);
+        }
+
+        @Override
+        public void onStopRtt() {
+            mRemote.stopRtt();
+        }
+
+        @Override
+        public void handleRttUpgradeResponse(RttTextStream rttTextStream) {
+            mRemote.sendRttUpgradeResponse(rttTextStream);
+        }
+
         private void setState(int state) {
             log("setState: " + state);
             switch (state) {
@@ -201,7 +246,6 @@
             }
         }
     }
-
     public final class TestManagedConference extends Conference {
         private final RemoteConference.Callback mRemoteCallback = new RemoteConference.Callback() {
             @Override
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
index 6c07073..71af9a8 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -25,7 +25,6 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.ParcelFileDescriptor;
 import android.support.v4.content.LocalBroadcastManager;
 import android.telecom.Conference;
 import android.telecom.Connection;
@@ -39,8 +38,6 @@
 import android.telecom.Log;
 import android.widget.Toast;
 
-import com.android.server.telecom.testapps.R;
-
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.List;
@@ -135,6 +132,8 @@
 
         /** Used to cleanup camera and media when done with connection. */
         private TestVideoProvider mTestVideoCallProvider;
+        private ConnectionRequest mOriginalRequest;
+        private RttChatbot mRttChatbot;
 
         private BroadcastReceiver mHangupReceiver = new BroadcastReceiver() {
             @Override
@@ -154,8 +153,16 @@
             }
         };
 
-        TestConnection(boolean isIncoming) {
+        private BroadcastReceiver mRttUpgradeReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                sendRemoteRttRequest();
+            }
+        };
+
+        TestConnection(boolean isIncoming, ConnectionRequest request) {
             mIsIncoming = isIncoming;
+            mOriginalRequest = request;
             // Assume all calls are video capable.
             int capabilities = getConnectionCapabilities();
             capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
@@ -167,6 +174,12 @@
             capabilities |= CAPABILITY_RESPOND_VIA_TEXT;
             setConnectionCapabilities(capabilities);
 
+            int properties = getConnectionProperties();
+            if (mOriginalRequest.isRequestingRtt()) {
+                properties |= PROPERTY_IS_RTT;
+            }
+            setConnectionProperties(properties);
+
             if (isIncoming) {
                 putExtra(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
             }
@@ -177,17 +190,24 @@
             filter.addDataScheme("int");
             LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
                     mUpgradeRequestReceiver, filter);
+
+            LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
+                    mRttUpgradeReceiver,
+                    new IntentFilter(TestCallActivity.ACTION_REMOTE_RTT_UPGRADE));
         }
 
         void startOutgoing() {
             setDialing();
-            mHandler.postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    setActive();
-                    activateCall(TestConnection.this);
-                }
+            mHandler.postDelayed(() -> {
+                setActive();
+                activateCall(TestConnection.this);
             }, 4000);
+            if (mOriginalRequest.isRequestingRtt()) {
+                Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
+                mRttChatbot = new RttChatbot(getApplicationContext(),
+                        mOriginalRequest.getRttTextStream());
+                mRttChatbot.start();
+            }
         }
 
         /** ${inheritDoc} */
@@ -204,6 +224,12 @@
             activateCall(this);
             setActive();
             updateConferenceable();
+            if (mOriginalRequest.isRequestingRtt()) {
+                Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
+                mRttChatbot = new RttChatbot(getApplicationContext(),
+                        mOriginalRequest.getRttTextStream());
+                mRttChatbot.start();
+            }
         }
 
         /** ${inheritDoc} */
@@ -246,6 +272,39 @@
             setActive();
         }
 
+        @Override
+        public void onStopRtt() {
+            int newProperties = getConnectionProperties() & ~PROPERTY_IS_RTT;
+            setConnectionProperties(newProperties);
+            mRttChatbot.stop();
+            mRttChatbot = null;
+        }
+
+        @Override
+        public void handleRttUpgradeResponse(RttTextStream rttTextStream) {
+            Log.i(this, "RTT request response was %s", rttTextStream == null);
+            if (rttTextStream != null) {
+                mRttChatbot = new RttChatbot(getApplicationContext(), rttTextStream);
+                mRttChatbot.start();
+                setConnectionProperties(getConnectionProperties() | PROPERTY_IS_RTT);
+                sendRttInitiationSuccess();
+            }
+        }
+
+        @Override
+        public void onStartRtt(RttTextStream textStream) {
+            boolean doAccept = Math.random() < 0.5;
+            if (doAccept) {
+                Log.i(this, "Accepting RTT request.");
+                mRttChatbot = new RttChatbot(getApplicationContext(), textStream);
+                mRttChatbot.start();
+                setConnectionProperties(getConnectionProperties() | PROPERTY_IS_RTT);
+                sendRttInitiationSuccess();
+            } else {
+                sendRttInitiationFailure(RttModifyStatus.SESSION_MODIFY_REQUEST_FAIL);
+            }
+        }
+
         public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
             mTestVideoCallProvider = testVideoCallProvider;
         }
@@ -273,8 +332,6 @@
 
     /** Used to play an audio tone during a call. */
     private MediaPlayer mMediaPlayer;
-    // Used to provide text reply in an RTT call
-    private RttChatbot mRttChatbot;
 
     @Override
     public boolean onUnbind(Intent intent) {
@@ -313,22 +370,12 @@
                     Toast.LENGTH_SHORT).show();
         }
 
-        if (originalRequest.isRequestingRtt()) {
-            Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
-            mRttChatbot = new RttChatbot(getApplicationContext(),
-                    originalRequest.getRttTextStream());
-            mRttChatbot.start();
-        }
-
         log("gateway package [" + gatewayPackage + "], original handle [" +
                 originalHandle + "]");
 
-        final TestConnection connection = new TestConnection(false /* isIncoming */);
+        final TestConnection connection =
+                new TestConnection(false /* isIncoming */, originalRequest);
         setAddress(connection, handle);
-        if (originalRequest.isRequestingRtt()) {
-            connection.setConnectionProperties(
-                    connection.getConnectionProperties() | Connection.PROPERTY_IS_RTT);
-        }
 
         // If the number starts with 555, then we handle it ourselves. If not, then we
         // use a remote connection service.
@@ -364,7 +411,7 @@
         ComponentName componentName = new ComponentName(this, TestConnectionService.class);
 
         if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
-            final TestConnection connection = new TestConnection(true);
+            final TestConnection connection = new TestConnection(true, request);
             // Get the stashed intent extra that determines if this is a video call or audio call.
             Bundle extras = request.getExtras();
             int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
@@ -393,12 +440,6 @@
                         "This is a test of call subject lines.");
             }
 
-            if (request.isRequestingRtt()) {
-                Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
-                mRttChatbot = new RttChatbot(getApplicationContext(), request.getRttTextStream());
-                mRttChatbot.start();
-            }
-
             connection.putExtras(connectionExtras);
 
             setAddress(connection, address);
@@ -421,7 +462,7 @@
         PhoneAccountHandle accountHandle = request.getAccountHandle();
         ComponentName componentName = new ComponentName(this, TestConnectionService.class);
         if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
-            final TestConnection connection = new TestConnection(false);
+            final TestConnection connection = new TestConnection(false, request);
             final Bundle extras = request.getExtras();
             final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
 
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index 809036c..2920ca7 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -25,6 +25,7 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.ListView;
+import android.widget.Toast;
 
 public class TestInCallUI extends Activity {
 
@@ -51,6 +52,28 @@
                     finish();
                 }
             }
+
+            @Override
+            public void onRttStarted(Call call) {
+                Toast.makeText(TestInCallUI.this, "RTT now enabled", Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttStopped(Call call) {
+                Toast.makeText(TestInCallUI.this, "RTT now disabled", Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttInitiationFailed(Call call, int reason) {
+                Toast.makeText(TestInCallUI.this, String.format("RTT failed to init: %d", reason),
+                        Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onRttRequest(Call call, int id) {
+                Toast.makeText(TestInCallUI.this, String.format("RTT request: %d", id),
+                        Toast.LENGTH_SHORT).show();
+            }
         });
 
         View endCallButton = findViewById(R.id.end_call_button);
@@ -58,6 +81,8 @@
         View muteButton = findViewById(R.id.mute_button);
         View rttIfaceButton = findViewById(R.id.rtt_iface_button);
         View answerButton = findViewById(R.id.answer_button);
+        View startRttButton = findViewById(R.id.start_rtt_button);
+        View acceptRttButton = findViewById(R.id.accept_rtt_button);
 
         endCallButton.setOnClickListener(new OnClickListener() {
             @Override
@@ -90,6 +115,7 @@
                 }
             }
         });
+
         rttIfaceButton.setOnClickListener((view) -> {
             Call call = mCallList.getCall(0);
             if (call.isRttActive()) {
@@ -98,12 +124,27 @@
                 startActivity(intent);
             }
         });
+
         answerButton.setOnClickListener(view -> {
             Call call = mCallList.getCall(0);
             if (call.getState() == Call.STATE_RINGING) {
                 call.answer(VideoProfile.STATE_AUDIO_ONLY);
             }
         });
+
+        startRttButton.setOnClickListener(view -> {
+            Call call = mCallList.getCall(0);
+            if (!call.isRttActive()) {
+                call.sendRttRequest();
+            }
+        });
+
+        acceptRttButton.setOnClickListener(view -> {
+            Call call = mCallList.getCall(0);
+            if (!call.isRttActive()) {
+                call.respondToRttRequest(mCallList.getLastRttRequestId(), true);
+            }
+        });
     }
 
     /** ${inheritDoc} */
diff --git a/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java b/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java
index ce962b4..9bb6977 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java
@@ -32,6 +32,7 @@
 import android.widget.EditText;
 import android.widget.Spinner;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -146,6 +147,11 @@
                     finish();
                 }
             }
+
+            @Override
+            public void onRttStopped(Call call) {
+                TestRttActivity.this.finish();
+            }
         });
 
         endRttButton.setOnClickListener((view) -> {
diff --git a/tests/Android.mk b/tests/Android.mk
index f4b8256..1e8b1af 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -20,6 +20,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
         android-ex-camera2 \
         android-support-v4 \
+        android-support-test \
         guava \
         mockito-target \
         platform-test-annotations \
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index f41c13f..86e5559 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.tests;
 
 import android.content.Context;
+import android.os.Build;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
 import android.telecom.InCallService;
@@ -208,7 +209,7 @@
                 mConnectionServiceFixtureA.mLatestConnectionId);
         InCallService.VideoCall videoCall =
                 mInCallServiceFixtureX.getCall(callIds.mCallId).getVideoCallImpl(
-                        mInCallServiceComponentNameX.getPackageName());
+                        mInCallServiceComponentNameX.getPackageName(), Build.VERSION.SDK_INT);
         videoCall.registerCallback(callback);
         ((VideoCallImpl) videoCall).setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
 
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
index b4a0a07..bba7c5a 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
@@ -541,6 +541,36 @@
     }
 
     @MediumTest
+    public void testListCurrentCallsHeldImsCepConference() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createHeldCall();
+        Call childCall1 = createActiveCall();
+        Call childCall2 = createActiveCall();
+        calls.add(parentCall);
+        calls.add(childCall1);
+        calls.add(childCall2);
+        addCallCapability(parentCall, Connection.CAPABILITY_MANAGE_CONFERENCE);
+        when(childCall1.getParentCall()).thenReturn(parentCall);
+        when(childCall2.getParentCall()).thenReturn(parentCall);
+
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getState()).thenReturn(CallState.ON_HOLD);
+        when(childCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(childCall2.getState()).thenReturn(CallState.ACTIVE);
+
+        when(parentCall.isIncoming()).thenReturn(true);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+        mBluetoothPhoneService.mBinder.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_HELD), eq(0),
+                eq(true), (String) isNull(), eq(-1));
+        verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(0), eq(CALL_STATE_HELD), eq(0),
+                eq(true), (String) isNull(), eq(-1));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @MediumTest
     public void testQueryPhoneState() throws Exception {
         Call ringingCall = createRingingCall();
         when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:5550000"));
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 1f444f2..14fdf05 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -164,6 +164,7 @@
 
         when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
         when(mockCallsManager.getLock()).thenReturn(mLock);
+        when(mockCallsManager.hasVideoCall()).thenReturn(false);
         when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
         when(fakeCall.isAlive()).thenReturn(true);
         when(fakeCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index 590304c..3bf044c 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -31,9 +31,9 @@
 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.support.test.filters.FlakyTest;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -424,7 +424,7 @@
     }
 
     @MediumTest
-    @Postsubmit
+    @FlakyTest
     public void testLogCallDirectionOutgoingWithMultiUserCapability() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mOtherUserAccountHandle,
@@ -518,7 +518,7 @@
     }
 
     @MediumTest
-    @Postsubmit
+    @FlakyTest
     public void testLogCallDirectionOutgoingFromManagedProfile() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle, 0));
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 9907ca7..f053a99 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -30,6 +30,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.telecom.CallAudioState;
 import android.telecom.Conference;
@@ -239,8 +240,9 @@
         }
 
         @Override
-        public void createConnectionFailed(String callId, ConnectionRequest request,
-                boolean isIncoming, Session.Info sessionInfo) throws RemoteException {
+        public void createConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount,
+                String callId, ConnectionRequest request, boolean isIncoming,
+                Session.Info sessionInfo) throws RemoteException {
             Log.i(ConnectionServiceFixture.this, "createConnectionFailed --> " + callId);
 
             if (mConnectionById.containsKey(callId)) {
@@ -328,6 +330,23 @@
         }
 
         @Override
+        public void startRtt(String callId, ParcelFileDescriptor fromInCall,
+                ParcelFileDescriptor toInCall, Session.Info sessionInfo) throws RemoteException {
+
+        }
+
+        @Override
+        public void stopRtt(String callId, Session.Info sessionInfo) throws RemoteException {
+
+        }
+
+        @Override
+        public void respondToRttUpgradeRequest(String callId, ParcelFileDescriptor fromInCall,
+                ParcelFileDescriptor toInCall, Session.Info sessionInfo) throws RemoteException {
+
+        }
+
+        @Override
         public IBinder asBinder() {
             return this;
         }
diff --git a/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
new file mode 100644
index 0000000..223cb58
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
@@ -0,0 +1,115 @@
+/* * Copyright (C) 2017 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.media.AudioManager;
+import android.media.ToneGenerator;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.DtmfLocalTonePlayer;
+import com.android.server.telecom.R;
+
+import org.mockito.Mock;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DtmfLocalTonePlayerTest extends TelecomTestCase {
+    private static final int TIMEOUT = 2000;
+    @Mock DtmfLocalTonePlayer.ToneGeneratorProxy mToneProxy;
+    @Mock Call mCall;
+
+    DtmfLocalTonePlayer mPlayer;
+
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        mPlayer = new DtmfLocalTonePlayer(mToneProxy);
+        when(mCall.getContext()).thenReturn(mContext);
+    }
+
+    @SmallTest
+    public void testSupportedStart() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(true);
+        when(mToneProxy.isPresent()).thenReturn(true);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy).create();
+    }
+
+    @SmallTest
+    public void testUnsupportedStart() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(false);
+        when(mToneProxy.isPresent()).thenReturn(true);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy, never()).create();
+    }
+
+    @SmallTest
+    public void testPlayToneWhenUninitialized() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(false);
+        when(mToneProxy.isPresent()).thenReturn(false);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        mPlayer.playTone(mCall, '9');
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy, never()).startTone(anyInt(), anyInt());
+    }
+
+    @SmallTest
+    public void testPlayToneWhenInitialized() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(true);
+        when(mToneProxy.isPresent()).thenReturn(true);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        mPlayer.playTone(mCall, '9');
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy).startTone(eq(ToneGenerator.TONE_DTMF_9), eq(-1));
+    }
+
+    @SmallTest
+    public void testStopToneWhenUninitialized() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(false);
+        when(mToneProxy.isPresent()).thenReturn(false);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        mPlayer.stopTone(mCall);
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy, never()).stopTone();
+    }
+
+    @SmallTest
+    public void testStopToneWhenInitialized() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(true);
+        when(mToneProxy.isPresent()).thenReturn(true);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        mPlayer.stopTone(mCall);
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy).stopTone();
+    }
+
+    @SmallTest
+    public void testProperTeardown() {
+        when(mContext.getResources().getBoolean(R.bool.allow_local_dtmf_tones)).thenReturn(true);
+        when(mToneProxy.isPresent()).thenReturn(true);
+        mPlayer.onForegroundCallChanged(null, mCall);
+        mPlayer.onForegroundCallChanged(mCall, null);
+        waitForHandlerAction(mPlayer.getHandler(), TIMEOUT);
+        verify(mToneProxy).release();
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 00760fe..7635427 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -125,6 +125,10 @@
         }
 
         @Override
+        public void onRttInitiationFailure(String callId, int reason) throws RemoteException {
+        }
+
+        @Override
         public IBinder asBinder() {
             return this;
         }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 6baaf85..3d2a52e 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -54,8 +54,8 @@
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
+import org.mockito.compat.ArgumentMatcher;
 import org.mockito.internal.matchers.VarargMatcher;
 
 import java.util.ArrayList;
@@ -114,14 +114,14 @@
         }
 
         @Override
-        public boolean matches(Object string) {
+        public boolean matchesObject(Object string) {
             return mStrings.contains(string);
         }
     }
 
     private static class IntVarArgMatcher extends ArgumentMatcher<int[]> implements VarargMatcher {
         @Override
-        public boolean matches(Object argument) {
+        public boolean matchesObject(Object argument) {
             return true;
         }
     }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index f590e8c..78d0fec 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -181,6 +181,11 @@
         public int getCurrentInterruptionFilter() {
             return mInterruptionFilter;
         }
+
+        @Override
+        public String getInterruptionModeInitiator() {
+            return "com.android.server.telecom";
+        }
     }
     @Mock HeadsetMediaButton mHeadsetMediaButton;
     @Mock ProximitySensorManager mProximitySensorManager;
diff --git a/tests/src/com/android/server/telecom/tests/VideoProfileTest.java b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
new file mode 100644
index 0000000..8e972e6
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 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.telecom.VideoProfile;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Unit tests for the {@link android.telecom.VideoProfile} class.
+ */
+public class VideoProfileTest extends AndroidTestCase {
+    @SmallTest
+    public void testToString() {
+        assertEquals("Audio Only", VideoProfile.videoStateToString(VideoProfile.STATE_AUDIO_ONLY));
+        assertEquals("Audio Tx", VideoProfile.videoStateToString(VideoProfile.STATE_TX_ENABLED));
+        assertEquals("Audio Rx", VideoProfile.videoStateToString(VideoProfile.STATE_RX_ENABLED));
+        assertEquals("Audio Tx Rx", VideoProfile.videoStateToString(
+                VideoProfile.STATE_BIDIRECTIONAL));
+
+        assertEquals("Audio Pause", VideoProfile.videoStateToString(VideoProfile.STATE_PAUSED));
+        assertEquals("Audio Tx Pause", VideoProfile.videoStateToString(
+                VideoProfile.STATE_TX_ENABLED | VideoProfile.STATE_PAUSED));
+        assertEquals("Audio Rx Pause", VideoProfile.videoStateToString(
+                VideoProfile.STATE_RX_ENABLED | VideoProfile.STATE_PAUSED));
+        assertEquals("Audio Tx Rx Pause", VideoProfile.videoStateToString(
+                VideoProfile.STATE_BIDIRECTIONAL | VideoProfile.STATE_PAUSED));
+    }
+
+    @SmallTest
+    public void testIsAudioOnly() {
+        assertFalse(VideoProfile.isAudioOnly(VideoProfile.STATE_RX_ENABLED));
+        assertFalse(VideoProfile.isAudioOnly(VideoProfile.STATE_TX_ENABLED));
+        assertFalse(VideoProfile.isAudioOnly(VideoProfile.STATE_BIDIRECTIONAL));
+
+        assertTrue(VideoProfile.isAudioOnly(VideoProfile.STATE_PAUSED));
+        assertTrue(VideoProfile.isAudioOnly(VideoProfile.STATE_AUDIO_ONLY));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index ee4890f..f6734c7 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.graphics.SurfaceTexture;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -106,7 +107,7 @@
         mVideoCallCallback = mock(InCallService.VideoCall.Callback.class);
 
         mVideoCall = mInCallServiceFixtureX.getCall(mCallIds.mCallId).getVideoCallImpl(
-                mInCallServiceComponentNameX.getPackageName());
+                mInCallServiceComponentNameX.getPackageName(), Build.VERSION.SDK_INT);
         mVideoCallImpl = (VideoCallImpl) mVideoCall;
         mVideoCall.registerCallback(mVideoCallCallback);