Implement mid-call RTT initiation and teardown

Adds functionality to testapps and to framework to allow for local and
remote initiation and disconnection of RTT.

Test: manual
Change-Id: I0e8248b495a7d3750c840591f1fa5388b34a32e2
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index d84bc2b..2e79478 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 =
@@ -426,6 +432,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.
@@ -1127,7 +1138,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;
@@ -2043,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) {
@@ -2060,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/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 8d393e8..69d9616 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.
      */
@@ -767,7 +766,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);
             }
         }
 
@@ -1012,7 +1011,7 @@
                     && extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
                 if (accountToUse != null
                         && accountToUse.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
-                    call.setIsRttCall(true);
+                    call.setRttStreams(true);
                 }
             }
         }
@@ -1465,7 +1464,7 @@
                         mPhoneAccountRegistrar.getPhoneAccountUnchecked(account);
                 if (realPhoneAccount != null
                         && realPhoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
-                    call.setIsRttCall(true);
+                    call.setRttStreams(true);
                 }
             }
 
@@ -2306,6 +2305,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 1d44a1d..cb6ff93 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();
@@ -870,7 +919,8 @@
                       mCallsManager.getEmergencyCallHelper().getLastEmergencyCallTimeMillis());
                 }
 
-                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
@@ -1222,6 +1272,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) {
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 0d2e30c..cbff31e 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;
@@ -612,6 +611,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() {
@@ -925,6 +935,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.
      */
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 2cdfc55..26735a6 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"
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/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index f24625e..5a7e500 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/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/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/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 9907ca7..77bf21d 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;
@@ -328,6 +329,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/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;
         }