Add support for RTT calls (part 1)

Adds logic for handling RTT call initiation and communication with the
InCallService and the ConnectionService and includes changes to the
testapps that exercise this functionality. This change handles RTT
initiation from the beginning of a call.

Change-Id: I3d713b662a100b2e0ad817b92005f044bcc60c62
Test: manual, through testapps
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 62b6d45..a786b55 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -23,6 +23,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.provider.ContactsContract.Contacts;
@@ -34,6 +35,7 @@
 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;
@@ -50,6 +52,7 @@
 import com.android.internal.telephony.SmsApplication;
 import com.android.internal.util.Preconditions;
 
+import java.io.IOException;
 import java.lang.String;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -82,6 +85,8 @@
     /** Identifies extras changes which originated from an incall service. */
     public static final int SOURCE_INCALL_SERVICE = 2;
 
+    private static final int RTT_PIPE_READ_SIDE_INDEX = 0;
+    private static final int RTT_PIPE_WRITE_SIDE_INDEX = 1;
     /**
      * Listener for events on the call.
      */
@@ -97,7 +102,7 @@
         void onPostDialWait(Call call, String remaining);
         void onPostDialChar(Call call, char nextChar);
         void onConnectionCapabilitiesChanged(Call call);
-        void onConnectionPropertiesChanged(Call call);
+        void onConnectionPropertiesChanged(Call call, boolean didRttChange);
         void onParentChanged(Call call);
         void onChildrenChanged(Call call);
         void onCannedSmsResponsesLoaded(Call call);
@@ -142,7 +147,7 @@
         @Override
         public void onConnectionCapabilitiesChanged(Call call) {}
         @Override
-        public void onConnectionPropertiesChanged(Call call) {}
+        public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {}
         @Override
         public void onParentChanged(Call call) {}
         @Override
@@ -407,6 +412,20 @@
     private String mOriginalConnectionId;
 
     /**
+     * Two pairs of {@link android.os.ParcelFileDescriptor}s that handle RTT text communication
+     * between the in-call app and the connection service. If both non-null, this call should be
+     * treated as an RTT call.
+     * Each array should be of size 2. First one is the read side and the second one is the write
+     * side.
+     */
+    private ParcelFileDescriptor[] mInCallToConnectionServiceStreams;
+    private ParcelFileDescriptor[] mConnectionServiceToInCallStreams;
+    /**
+     * Integer constant from {@link android.telecom.Call.RttCall}. Describes the current RTT mode.
+     */
+    private int mRttMode;
+
+    /**
      * Persists the specified parameters and initializes the new instance.
      *
      * @param context The context.
@@ -1087,11 +1106,17 @@
             connectionProperties &= ~Connection.PROPERTY_SELF_MANAGED;
         }
 
-        if (mConnectionProperties != connectionProperties) {
+        int changedProperties = mConnectionProperties ^ connectionProperties;
+
+        if (changedProperties != 0) {
             int previousProperties = mConnectionProperties;
             mConnectionProperties = connectionProperties;
+            setIsRttCall((mConnectionProperties & Connection.PROPERTY_IS_RTT) ==
+                    Connection.PROPERTY_IS_RTT);
+            boolean didRttChange =
+                    (changedProperties & Connection.PROPERTY_IS_RTT) == Connection.PROPERTY_IS_RTT;
             for (Listener l : mListeners) {
-                l.onConnectionPropertiesChanged(this);
+                l.onConnectionPropertiesChanged(this, didRttChange);
             }
 
             boolean wasExternal = (previousProperties & Connection.PROPERTY_IS_EXTERNAL_CALL)
@@ -1105,7 +1130,6 @@
                 for (Listener l : mListeners) {
                     l.onExternalCallChanged(this, isExternal);
                 }
-
             }
 
             mAnalytics.addCallProperties(mConnectionProperties);
@@ -2003,6 +2027,55 @@
         return mSpeakerphoneOn;
     }
 
+    public void setIsRttCall(boolean shouldBeRtt) {
+        boolean areStreamsInitialized = mInCallToConnectionServiceStreams != null
+                && mConnectionServiceToInCallStreams != null;
+        if (shouldBeRtt && !areStreamsInitialized) {
+            try {
+                mInCallToConnectionServiceStreams = ParcelFileDescriptor.createReliablePipe();
+                mConnectionServiceToInCallStreams = ParcelFileDescriptor.createReliablePipe();
+            } catch (IOException e) {
+                Log.e(this, e, "Failed to create pipes for RTT call.");
+            }
+        } else if (!shouldBeRtt && areStreamsInitialized) {
+            closeRttPipes();
+            mInCallToConnectionServiceStreams = null;
+            mConnectionServiceToInCallStreams = null;
+        }
+    }
+
+    public void closeRttPipes() {
+        // TODO: may defer this until call is removed?
+    }
+
+    public boolean isRttCall() {
+        return (mConnectionProperties & Connection.PROPERTY_IS_RTT) == Connection.PROPERTY_IS_RTT;
+    }
+
+    public ParcelFileDescriptor getCsToInCallRttPipeForCs() {
+        return mConnectionServiceToInCallStreams == null ? null
+                : mConnectionServiceToInCallStreams[RTT_PIPE_WRITE_SIDE_INDEX];
+    }
+
+    public ParcelFileDescriptor getInCallToCsRttPipeForCs() {
+        return mInCallToConnectionServiceStreams == null ? null
+                : mInCallToConnectionServiceStreams[RTT_PIPE_READ_SIDE_INDEX];
+    }
+
+    public ParcelFileDescriptor getCsToInCallRttPipeForInCall() {
+        return mConnectionServiceToInCallStreams == null ? null
+                : mConnectionServiceToInCallStreams[RTT_PIPE_READ_SIDE_INDEX];
+    }
+
+    public ParcelFileDescriptor getInCallToCsRttPipeForInCall() {
+        return mInCallToConnectionServiceStreams == null ? null
+                : mInCallToConnectionServiceStreams[RTT_PIPE_WRITE_SIDE_INDEX];
+    }
+
+    public int getRttMode() {
+        return mRttMode;
+    }
+
     /**
      * Sets a video call provider for the call.
      */
@@ -2221,6 +2294,11 @@
         return mCallDataUsage;
     }
 
+    public void setRttMode(int mode) {
+        mRttMode = mode;
+        // TODO: hook this up to CallAudioManager
+    }
+
     /**
      * Returns true if the call is outgoing and the NEW_OUTGOING_CALL ordered broadcast intent
      * has come back to telecom and was processed.
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 0060680..c30641b 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -720,6 +720,12 @@
         if (phoneAccount != null) {
             call.setIsSelfManaged(phoneAccount.isSelfManaged());
         }
+        if (extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
+            if (phoneAccount != null &&
+                    phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
+                call.setIsRttCall(true);
+            }
+        }
 
         call.initAnalytics();
         if (getForegroundCall() != null) {
@@ -854,9 +860,9 @@
             isReusedCall = false;
         }
 
-        // Set the video state on the call early so that when it is added to the InCall UI the UI
-        // knows to configure itself as a video call immediately.
         if (extras != null) {
+            // Set the video state on the call early so that when it is added to the InCall UI the
+            // UI knows to configure itself as a video call immediately.
             int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
                     VideoProfile.STATE_AUDIO_ONLY);
 
@@ -956,8 +962,16 @@
             call.setState(
                     CallState.CONNECTING,
                     phoneAccountHandle == null ? "no-handle" : phoneAccountHandle.toString());
+            PhoneAccount accountToUse =
+                    mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
+            if (extras != null
+                    && extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
+                if (accountToUse != null
+                        && accountToUse.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
+                    call.setIsRttCall(true);
+                }
+            }
         }
-
         setIntentExtrasAndStartTime(call, extras);
 
         // Do not add the call if it is a potential MMI code.
@@ -1401,6 +1415,16 @@
         } else {
             call.setTargetPhoneAccount(account);
 
+            if (call.getIntentExtras()
+                    .getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
+                PhoneAccount realPhoneAccount =
+                        mPhoneAccountRegistrar.getPhoneAccountUnchecked(account);
+                if (realPhoneAccount != null
+                        && realPhoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
+                    call.setIsRttCall(true);
+                }
+            }
+
             if (!call.isNewOutgoingCallIntentBroadcastDone()) {
                 return;
             }
@@ -1829,6 +1853,7 @@
         call.setParentCall(null);  // need to clean up parent relationship before destroying.
         call.removeListener(this);
         call.clearConnectionService();
+        // TODO: clean up RTT pipes
 
         boolean shouldNotify = false;
         if (mCalls.contains(call)) {
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index e4129ae..be54f4f 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -861,24 +861,30 @@
                 }
 
                 Log.addEvent(call, LogUtils.Events.START_CONNECTION, Log.piiHandle(call.getHandle()));
-                try {
-                    // 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
-                    // new incoming call.
-                    boolean shouldShowIncomingCallUI = call.isSelfManaged() &&
-                            !mCallsManager.hasCallsForOtherPhoneAccount(
-                                    call.getTargetPhoneAccount());
 
+                // 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
+                // new incoming call.
+                boolean shouldShowIncomingCallUI = call.isSelfManaged() &&
+                        !mCallsManager.hasCallsForOtherPhoneAccount(
+                                call.getTargetPhoneAccount());
+
+                ConnectionRequest connectionRequest = new ConnectionRequest.Builder()
+                        .setAccountHandle(call.getTargetPhoneAccount())
+                        .setAddress(call.getHandle())
+                        .setExtras(call.getIntentExtras())
+                        .setVideoState(call.getVideoState())
+                        .setTelecomCallId(callId)
+                        .setShouldShowIncomingCallUi(shouldShowIncomingCallUI)
+                        .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
+                        .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
+                        .build();
+
+                try {
                     mServiceInterface.createConnection(
                             call.getConnectionManagerPhoneAccount(),
                             callId,
-                            new ConnectionRequest(
-                                    call.getTargetPhoneAccount(),
-                                    call.getHandle(),
-                                    extras,
-                                    call.getVideoState(),
-                                    callId,
-                                    shouldShowIncomingCallUI),
+                            connectionRequest,
                             call.shouldAttachToExistingConnection(),
                             call.isUnknown(),
                             Log.getExternalSession());
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index e775818..4b54760 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -494,4 +494,72 @@
              Log.endSession();
         }
     }
+
+    @Override
+    public void sendRttRequest() {
+        try {
+            Log.startSession("ICA.sRR");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // TODO
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
+    public void respondToRttRequest(int id, boolean accept) {
+        try {
+            Log.startSession("ICA.rTRR");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // TODO
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
+    public void stopRtt() {
+        try {
+            Log.startSession("ICA.sRTT");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // TODO
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
+    public void setRttMode(int mode) {
+        try {
+            Log.startSession("ICA.sRM");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    // TODO
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index ec46800..dce4364 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -29,6 +29,7 @@
 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;
@@ -76,6 +77,7 @@
         public void setListener(Listener l) {
             mListener = l;
         }
+        public InCallServiceInfo getInfo() { return null; }
         public void dump(IndentingPrintWriter pw) {}
     }
 
@@ -211,6 +213,11 @@
         }
 
         @Override
+        public InCallServiceInfo getInfo() {
+            return mInCallServiceInfo;
+        }
+
+        @Override
         public void disconnect() {
             if (mIsConnected) {
                 mContext.unbindService(mServiceConnection);
@@ -319,6 +326,14 @@
         }
 
         @Override
+        public InCallServiceInfo getInfo() {
+            if (mIsProxying) {
+                return mSubConnection.getInfo();
+            } else {
+                return super.getInfo();
+            }
+        }
+        @Override
         protected void onDisconnected() {
             // Save this here because super.onDisconnected() could force us to explicitly
             // disconnect() as a cleanup step and that sets mIsConnected to false.
@@ -427,6 +442,11 @@
         }
 
         @Override
+        public InCallServiceInfo getInfo() {
+            return mCurrentConnection.getInfo();
+        }
+
+        @Override
         public void dump(IndentingPrintWriter pw) {
             pw.println("Car Swapping ICS");
             pw.increaseIndent();
@@ -490,8 +510,8 @@
         }
 
         @Override
-        public void onConnectionPropertiesChanged(Call call) {
-            updateCall(call);
+        public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {
+            updateCall(call, false /* includeVideoProvider */, didRttChange);
         }
 
         @Override
@@ -501,7 +521,7 @@
 
         @Override
         public void onVideoCallProviderChanged(Call call) {
-            updateCall(call, true /* videoProviderChanged */);
+            updateCall(call, true /* videoProviderChanged */, false);
         }
 
         @Override
@@ -660,12 +680,15 @@
                     continue;
                 }
 
+                // Only send the RTT call if it's a UI in-call service
+                boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
+
                 componentsUpdated.add(info.getComponentName());
                 IInCallService inCallService = entry.getValue();
 
                 ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
                         true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
-                        info.isExternalCallsSupported());
+                        info.isExternalCallsSupported(), includeRttCall);
                 try {
                     inCallService.addCall(parcelableCall);
                 } catch (RemoteException ignored) {
@@ -728,9 +751,12 @@
                 componentsUpdated.add(info.getComponentName());
                 IInCallService inCallService = entry.getValue();
 
+                // Only send the RTT call if it's a UI in-call service
+                boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
+
                 ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
                         true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
-                        info.isExternalCallsSupported());
+                        info.isExternalCallsSupported(), includeRttCall);
                 try {
                     inCallService.addCall(parcelableCall);
                 } catch (RemoteException ignored) {
@@ -746,7 +772,8 @@
                     false /* includeVideoProvider */,
                     mCallsManager.getPhoneAccountRegistrar(),
                     false /* supportsExternalCalls */,
-                    android.telecom.Call.STATE_DISCONNECTED /* overrideState */);
+                    android.telecom.Call.STATE_DISCONNECTED /* overrideState */,
+                    false /* includeRttCall */);
 
             Log.i(this, "Removing external call %s ==> %s", call, parcelableCall);
             for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
@@ -1138,6 +1165,9 @@
                     continue;
                 }
 
+                // Only send the RTT call if it's a UI in-call service
+                boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
+
                 // Track the call if we don't already know about it.
                 addCall(call);
                 numCallsSent += 1;
@@ -1145,7 +1175,8 @@
                         call,
                         true /* includeVideoProvider */,
                         mCallsManager.getPhoneAccountRegistrar(),
-                        info.isExternalCallsSupported()));
+                        info.isExternalCallsSupported(),
+                        includeRttCall));
             } catch (RemoteException ignored) {
             }
         }
@@ -1176,7 +1207,7 @@
      * @param call The {@link Call}.
      */
     private void updateCall(Call call) {
-        updateCall(call, false /* videoProviderChanged */);
+        updateCall(call, false /* videoProviderChanged */, false);
     }
 
     /**
@@ -1185,8 +1216,10 @@
      * @param call The {@link Call}.
      * @param videoProviderChanged {@code true} if the video provider changed, {@code false}
      *      otherwise.
+     * @param rttInfoChanged {@code true} if any information about the RTT session changed,
+     * {@code false} otherwise.
      */
-    private void updateCall(Call call, boolean videoProviderChanged) {
+    private void updateCall(Call call, boolean videoProviderChanged, boolean rttInfoChanged) {
         if (!mInCallServices.isEmpty()) {
             Log.i(this, "Sending updateCall %s", call);
             List<ComponentName> componentsUpdated = new ArrayList<>();
@@ -1200,7 +1233,8 @@
                         call,
                         videoProviderChanged /* includeVideoProvider */,
                         mCallsManager.getPhoneAccountRegistrar(),
-                        info.isExternalCallsSupported());
+                        info.isExternalCallsSupported(),
+                        rttInfoChanged && info.equals(mInCallServiceConnection.getInfo()));
                 ComponentName componentName = info.getComponentName();
                 IInCallService inCallService = entry.getValue();
                 componentsUpdated.add(componentName);
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index c4ea9cf..e855549 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -19,6 +19,7 @@
 import android.net.Uri;
 import android.telecom.Connection;
 import android.telecom.ParcelableCall;
+import android.telecom.ParcelableRttCall;
 import android.telecom.TelecomManager;
 
 import java.util.ArrayList;
@@ -34,7 +35,7 @@
         public ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider,
                 PhoneAccountRegistrar phoneAccountRegistrar) {
             return ParcelableCallUtils.toParcelableCall(
-                    call, includeVideoProvider, phoneAccountRegistrar, false);
+                    call, includeVideoProvider, phoneAccountRegistrar, false, false);
         }
     }
 
@@ -54,9 +55,11 @@
             Call call,
             boolean includeVideoProvider,
             PhoneAccountRegistrar phoneAccountRegistrar,
-            boolean supportsExternalCalls) {
+            boolean supportsExternalCalls,
+            boolean includeRttCall) {
         return toParcelableCall(call, includeVideoProvider, phoneAccountRegistrar,
-                supportsExternalCalls, CALL_STATE_OVERRIDE_NONE /* overrideState */);
+                supportsExternalCalls, CALL_STATE_OVERRIDE_NONE /* overrideState */,
+                includeRttCall);
     }
 
     /**
@@ -79,7 +82,8 @@
             boolean includeVideoProvider,
             PhoneAccountRegistrar phoneAccountRegistrar,
             boolean supportsExternalCalls,
-            int overrideState) {
+            int overrideState,
+            boolean includeRttCall) {
         int state;
         if (overrideState == CALL_STATE_OVERRIDE_NONE) {
             state = getParcelableState(call, supportsExternalCalls);
@@ -152,6 +156,8 @@
             conferenceableCallIds.add(otherCall.getId());
         }
 
+        ParcelableRttCall rttCall = includeRttCall ? getParcelableRttCall(call) : null;
+
         return new ParcelableCall(
                 call.getId(),
                 state,
@@ -169,6 +175,8 @@
                 call.getTargetPhoneAccount(),
                 includeVideoProvider,
                 includeVideoProvider ? call.getVideoProvider() : null,
+                includeRttCall,
+                rttCall,
                 parentCallId,
                 childCallIds,
                 call.getStatusHints(),
@@ -346,5 +354,13 @@
         return capabilities & ~capability;
     }
 
+    private static ParcelableRttCall getParcelableRttCall(Call call) {
+        if (!call.isRttCall()) {
+            return null;
+        }
+        return new ParcelableRttCall(call.getRttMode(), call.getInCallToCsRttPipeForInCall(),
+                call.getCsToInCallRttPipeForInCall());
+    }
+
     private ParcelableCallUtils() {}
 }
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 03c1d60..a6a0d76 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -81,6 +81,16 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="com.android.server.telecom.testapps.TestRttActivity"
+                  android:process="com.android.server.telecom.testapps.TestInCallService"
+                  android:label="@string/rttUiLabel"
+                  android:launchMode="singleInstance">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="com.android.server.telecom.testapps.TestCallActivity"
                   android:theme="@android:style/Theme.NoDisplay"
                   android:label="@string/testCallActivityLabel">
@@ -105,6 +115,11 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:scheme="int" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="android.telecom.testapps.ACTION_RTT_CALL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="tel" />
+            </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 6a891e7..3ca8781 100644
--- a/testapps/res/layout/incall_screen.xml
+++ b/testapps/res/layout/incall_screen.xml
@@ -44,7 +44,16 @@
             android:id="@+id/hold_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/holdButton" >
-        </Button>
+            android:text="@string/holdButton"/>
+        <Button
+            android:id="@+id/rtt_iface_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/rttIfaceButton"/>
+        <Button
+            android:id="@+id/answer_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/answerCallButton"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/testapps/res/layout/rtt_incall_screen.xml b/testapps/res/layout/rtt_incall_screen.xml
new file mode 100644
index 0000000..e7cbac4
--- /dev/null
+++ b/testapps/res/layout/rtt_incall_screen.xml
@@ -0,0 +1,57 @@
+<?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:id="@+id/received_messages_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:text="> "
+        android:lines="7" />
+    <TextView
+        android:id="@+id/sent_messages_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:text="> "
+        android:lines="7" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/end_rtt_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/endRttButton" />
+        <Spinner
+            android:id="@+id/rtt_mode_selection_spinner"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+
+    <EditText
+        android:id="@+id/rtt_typing_box"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:lines="1"
+        android:hint="Type here" />
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
index 2c3e5e4..e6c56b7 100644
--- a/testapps/res/layout/testdialer_main.xml
+++ b/testapps/res/layout/testdialer_main.xml
@@ -44,4 +44,9 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/cancelMissedButton" />
+    <CheckBox
+        android:id="@+id/call_with_rtt_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/startCallWithRtt"/>
 </LinearLayout>
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index b9c2aa0..56cb476 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -40,6 +40,14 @@
 
     <string name="endCallButton">End Call</string>
 
+    <string name="answerCallButton">Answer</string>
+
+    <string name="startCallWithRtt">Start call with RTT</string>
+
+    <string name="rttIfaceButton">RTT</string>
+
+    <string name="endRttButton">End RTT</string>
+
     <string name="muteButton">Mute</string>
 
     <string name="holdButton">Hold</string>
@@ -58,4 +66,22 @@
     <string name="incomingCallNotPermitted">Incoming call not permitted.</string>
 
     <string name="incomingCallNotPermittedCS">Incoming call not permitted (CS Reported).</string>
+
+    <string name="rttUiLabel">Test RTT UI</string>
+
+    <string-array name="rtt_mode_array">
+        <item>Full</item>
+        <item>HCO</item>
+        <item>VCO</item>
+    </string-array>
+
+    <string-array name="rtt_reply_one_liners">
+        <item>To RTT or not to RTT, that is the question...</item>
+        <item>Making TTY great again!</item>
+        <item>I would be more comfortable with real "Thyme" chatting. I don\'t know how to end
+        this pun</item>
+        <item>お疲れ様でした</item>
+        <item>The FCC has mandated that I respond... I will do so begrudgingly</item>
+        <item>😂😂😂💯</item>
+    </string-array>
 </resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
index bea0e63..85785d5 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
@@ -103,7 +103,8 @@
 
         state.setText(getStateString(call));
 
-        Log.i(TAG, "Call found: " + handle.getSchemeSpecificPart() + ", " + durationMs);
+        Log.i(TAG, "Call found: " + ((handle == null) ? "null" : handle.getSchemeSpecificPart())
+                + ", " + durationMs);
 
         return convertView;
     }
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
index aee5514..8fd2378 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -51,6 +51,8 @@
             "com.android.server.telecom.testapps.ACTION_TWO_WAY_VIDEO_CALL";
     static final String ACTION_AUDIO_CALL =
             "com.android.server.telecom.testapps.ACTION_AUDIO_CALL";
+    static final String ACTION_RTT_CALL =
+            "com.android.server.telecom.testapps.ACTION_RTT_CALL";
 
     /** {@inheritDoc} */
     @Override
@@ -66,6 +68,8 @@
             sendIncomingCallIntent(context, null, VideoProfile.STATE_RX_ENABLED);
         } else if (ACTION_TWO_WAY_VIDEO_CALL.equals(action)) {
             sendIncomingCallIntent(context, null, VideoProfile.STATE_BIDIRECTIONAL);
+        } else if (ACTION_RTT_CALL.equals(action)) {
+            sendIncomingRttCallIntent(context, null, VideoProfile.STATE_AUDIO_ONLY);
         } else if (ACTION_AUDIO_CALL.equals(action)) {
             sendIncomingCallIntent(context, null, VideoProfile.STATE_AUDIO_ONLY);
         }
@@ -93,6 +97,23 @@
         TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
     }
 
+    public static void sendIncomingRttCallIntent(Context context, Uri handle, int videoState) {
+        PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
+                new ComponentName(context, TestConnectionService.class),
+                CallServiceNotifier.SIM_SUBSCRIPTION_ID);
+
+        // For the purposes of testing, indicate whether the incoming call is a video call by
+        // stashing an indicator in the EXTRA_INCOMING_CALL_EXTRAS.
+        Bundle extras = new Bundle();
+        extras.putInt(TestConnectionService.EXTRA_START_VIDEO_STATE, videoState);
+        if (handle != null) {
+            extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
+        }
+        extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
+
+        TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+    }
+
     public static void addNewUnknownCall(Context context, Uri handle, Bundle extras) {
         Log.i(TAG, "Adding new unknown call with handle " + handle);
         PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
diff --git a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
index 9292273..ba58655 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
@@ -119,6 +119,7 @@
                 .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
                 .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
                         PhoneAccount.CAPABILITY_VIDEO_CALLING |
+                        PhoneAccount.CAPABILITY_RTT |
                         PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)
                 .setIcon(Icon.createWithResource(
                         context.getResources(), R.drawable.stat_sys_phone_call))
@@ -139,6 +140,7 @@
                 .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
                         PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
                         PhoneAccount.CAPABILITY_VIDEO_CALLING |
+                        PhoneAccount.CAPABILITY_RTT |
                         PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)
                 .setIcon(Icon.createWithResource(
                         context.getResources(), R.drawable.stat_sys_phone_call))
diff --git a/testapps/src/com/android/server/telecom/testapps/RttChatbot.java b/testapps/src/com/android/server/telecom/testapps/RttChatbot.java
new file mode 100644
index 0000000..3b16bd4
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/RttChatbot.java
@@ -0,0 +1,134 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.telecom.Connection;
+import android.telecom.Log;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.Random;
+
+public class RttChatbot {
+    private static final String LOG_TAG = RttChatbot.class.getSimpleName();
+    private static final long PER_CHARACTER_DELAY_MS = 100;
+    private static final long MSG_WAIT_DELAY_MS = 3999;
+    private static final double ONE_LINER_FREQUENCY = 0.1;
+    private static final String REPLY_PREFIX = "You said: ";
+
+    private static final int BEGIN_SEND_REPLY_MESSAGE = 1;
+    private static final int SEND_CHARACTER = 2;
+    private static final int APPEND_TO_INPUT_BUFFER = 3;
+
+    private final Connection.RttTextStream mRttTextStream;
+    private final Random mRandom = new Random();
+    private final String[] mOneLiners;
+    private Handler mHandler;
+
+    private final class ReplyHandler extends Handler {
+        private StringBuilder mInputSoFar;
+
+        public ReplyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case BEGIN_SEND_REPLY_MESSAGE:
+                    removeMessages(SEND_CHARACTER);
+                    sendReplyMessage();
+                    break;
+                case SEND_CHARACTER:
+                    try {
+                        mRttTextStream.write((String) msg.obj);
+                    } catch (IOException e) {
+                    }
+                    break;
+                case APPEND_TO_INPUT_BUFFER:
+                    removeMessages(BEGIN_SEND_REPLY_MESSAGE);
+                    sendEmptyMessageDelayed(BEGIN_SEND_REPLY_MESSAGE, MSG_WAIT_DELAY_MS);
+                    String toAppend = (String) msg.obj;
+                    if (mInputSoFar == null) {
+                        mInputSoFar = new StringBuilder(toAppend);
+                    } else {
+                        mInputSoFar.append(toAppend);
+                    }
+                    Log.d(LOG_TAG, "Got %s to append, total text now %s",
+                            toAppend, mInputSoFar.toString());
+                    break;
+            }
+        }
+
+        private void sendReplyMessage() {
+            String messageToSend;
+            if (mRandom.nextDouble() < ONE_LINER_FREQUENCY) {
+                messageToSend = mOneLiners[mRandom.nextInt(mOneLiners.length)];
+            } else {
+                messageToSend = REPLY_PREFIX + mInputSoFar.toString();
+            }
+            mInputSoFar = null;
+            Log.i(LOG_TAG, "Begin send reply message: %s", messageToSend);
+            int[] charsToSend = messageToSend.codePoints().toArray();
+            for (int i = 0; i < charsToSend.length; i++) {
+                Message msg = obtainMessage(SEND_CHARACTER,
+                        new String(new int[] {charsToSend[i]}, 0, 1));
+                sendMessageDelayed(msg, PER_CHARACTER_DELAY_MS * i);
+            }
+        }
+    }
+
+    public RttChatbot(Context context, Connection.RttTextStream textStream) {
+        mOneLiners = context.getResources().getStringArray(R.array.rtt_reply_one_liners);
+        mRttTextStream = textStream;
+    }
+
+    public void start() {
+        Log.i(LOG_TAG, "Starting RTT chatbot.");
+        HandlerThread ht = new HandlerThread("RttChatbotSender");
+        ht.start();
+        mHandler = new ReplyHandler(ht.getLooper());
+        Thread receiveThread = new Thread(() -> {
+            while (true) {
+                String charsReceived = mRttTextStream.read();
+                if (charsReceived == null) {
+                    if (Thread.currentThread().isInterrupted()) {
+                        Log.w(LOG_TAG, "Thread interrupted");
+                        break;
+                    }
+                    Log.w(LOG_TAG, "Stream closed");
+                    break;
+                }
+                if (charsReceived.length() == 0) {
+                    continue;
+                }
+                mHandler.obtainMessage(APPEND_TO_INPUT_BUFFER, charsReceived)
+                        .sendToTarget();
+            }
+        }, "RttChatbotReceiver");
+        receiveThread.start();
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
index 862ccf7..76f2058 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
@@ -52,6 +52,9 @@
     public static final String ACTION_SEND_UPGRADE_REQUEST =
             "android.telecom.testapps.ACTION_SEND_UPGRADE_REQUEST";
 
+    static final String ACTION_RTT_CALL =
+            "android.telecom.testapps.ACTION_RTT_CALL";
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -65,6 +68,9 @@
             CallNotificationReceiver.addNewUnknownCall(this, data, intent.getExtras());
         } else if (ACTION_HANGUP_CALLS.equals(action)) {
             CallNotificationReceiver.hangupCalls(this);
+        } else if (ACTION_RTT_CALL.equals(action)) {
+            CallNotificationReceiver.sendIncomingRttCallIntent(
+                    this, data, VideoProfile.STATE_AUDIO_ONLY);
         } else if (ACTION_SEND_UPGRADE_REQUEST.equals(action)) {
             CallNotificationReceiver.sendUpgradeRequest(this, data);
         } else {
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallList.java b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
index 391a588..4419b17 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
@@ -32,7 +32,7 @@
 /**
  * Maintains a list of calls received via the {@link TestInCallServiceImpl}.
  */
-public class TestCallList extends Call.Listener {
+public class TestCallList extends Call.Callback {
 
     public static abstract class Listener {
         public void onCallAdded(Call call) {}
@@ -126,7 +126,7 @@
         }
         Log.i(TAG, "addCall: " + call + " " + System.identityHashCode(this));
         mCalls.add(call);
-        call.addListener(this);
+        call.registerCallback(this);
 
         for (Listener l : mListeners) {
             l.onCallAdded(call);
@@ -140,7 +140,7 @@
         }
         Log.i(TAG, "removeCall: " + call);
         mCalls.remove(call);
-        call.removeListener(this);
+        call.unregisterCallback(this);
 
         for (Listener l : mListeners) {
             l.onCallRemoved(call);
@@ -214,4 +214,15 @@
             }
         }
     }
+
+    @Override
+    public void onRttStatusChanged(Call call, boolean enabled, Call.RttCall rttCall) {
+        Log.v(TAG, "onRttStatusChanged: call = " + call + " " + System.identityHashCode(this));
+
+        if (call != null) {
+            // Did you have another call? Well too bad, this class isn't gonna handle it.
+            mCalls.clear();
+            mCalls.add(call);
+        }
+    }
 }
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
index 5b0cf63..c2d8852 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
@@ -135,6 +135,7 @@
             mRemote.registerCallback(mRemoteCallback);
             setState(mRemote.getState());
             setVideoState(mRemote.getVideoState());
+            setConnectionProperties(remote.getConnectionProperties());
         }
 
         @Override
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
index 0150dbe..6c07073 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -25,6 +25,7 @@
 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;
@@ -35,7 +36,7 @@
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
-import android.util.Log;
+import android.telecom.Log;
 import android.widget.Toast;
 
 import com.android.server.telecom.testapps.R;
@@ -57,6 +58,7 @@
 
     public static final String EXTRA_HANDLE = "extra_handle";
 
+    private static final String LOG_TAG = TestConnectionService.class.getSimpleName();
     /**
      * Random number generator used to generate phone numbers.
      */
@@ -271,6 +273,8 @@
 
     /** 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) {
@@ -309,11 +313,22 @@
                     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 */);
         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.
@@ -377,6 +392,13 @@
                 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
                         "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);
@@ -385,11 +407,6 @@
 
             addCall(connection);
 
-            ConnectionRequest newRequest = new ConnectionRequest(
-                    request.getAccountHandle(),
-                    address,
-                    request.getExtras(),
-                    videoState);
             connection.setVideoState(videoState);
             return connection;
         } else {
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
index 596d18d..c7eccf7 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -12,15 +12,15 @@
 import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.CheckBox;
 import android.widget.EditText;
 import android.widget.Toast;
 
-import com.android.server.telecom.testapps.R;
-
 public class TestDialerActivity extends Activity {
     private static final int REQUEST_CODE_SET_DEFAULT_DIALER = 1;
 
     private EditText mNumberView;
+    private CheckBox mRttCheckbox;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -55,7 +55,8 @@
         });
 
         mNumberView = (EditText) findViewById(R.id.number);
-        updateEditTextWithNumber();
+        mRttCheckbox = (CheckBox) findViewById(R.id.call_with_rtt_checkbox);
+        updateMutableUi();
     }
 
     @Override
@@ -72,13 +73,15 @@
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
-        updateEditTextWithNumber();
+        updateMutableUi();
     }
 
-    private void updateEditTextWithNumber() {
+    private void updateMutableUi() {
         Intent intent = getIntent();
         if (intent != null) {
             mNumberView.setText(intent.getDataString());
+            mRttCheckbox.setChecked(
+                    intent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_RTT, false));
         }
     }
 
@@ -127,7 +130,10 @@
 
     private Bundle createCallIntentExtras() {
         Bundle extras = new Bundle();
-        extras.putString("com.android.server.telecom.testapps.CALL_EXTRAS", "Yorke was here");
+        extras.putString("com.android.server.telecom.testapps.CALL_EXTRAS", "Hall was here");
+        if (mRttCheckbox.isChecked()) {
+            extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
+        }
 
         Bundle intentExtras = new Bundle();
         intentExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index ce53709..809036c 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -17,9 +17,10 @@
 package com.android.server.telecom.testapps;
 
 import android.app.Activity;
-import android.graphics.Color;
+import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.Call;
+import android.telecom.VideoProfile;
 import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -46,7 +47,7 @@
             @Override
             public void onCallRemoved(Call call) {
                 if (mCallList.size() == 0) {
-                    Log.i("Santos", "Ending the incall UI");
+                    Log.i(TestInCallUI.class.getSimpleName(), "Ending the incall UI");
                     finish();
                 }
             }
@@ -55,6 +56,8 @@
         View endCallButton = findViewById(R.id.end_call_button);
         View holdButton = findViewById(R.id.hold_button);
         View muteButton = findViewById(R.id.mute_button);
+        View rttIfaceButton = findViewById(R.id.rtt_iface_button);
+        View answerButton = findViewById(R.id.answer_button);
 
         endCallButton.setOnClickListener(new OnClickListener() {
             @Override
@@ -83,9 +86,24 @@
             public void onClick(View view) {
                 Call call = mCallList.getCall(0);
                 if (call != null) {
+
                 }
             }
         });
+        rttIfaceButton.setOnClickListener((view) -> {
+            Call call = mCallList.getCall(0);
+            if (call.isRttActive()) {
+                Intent intent = new Intent(Intent.ACTION_MAIN);
+                intent.setClass(this, TestRttActivity.class);
+                startActivity(intent);
+            }
+        });
+        answerButton.setOnClickListener(view -> {
+            Call call = mCallList.getCall(0);
+            if (call.getState() == Call.STATE_RINGING) {
+                call.answer(VideoProfile.STATE_AUDIO_ONLY);
+            }
+        });
     }
 
     /** ${inheritDoc} */
diff --git a/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java b/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java
new file mode 100644
index 0000000..ce962b4
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestRttActivity.java
@@ -0,0 +1,229 @@
+/*
+ * 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.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telecom.Call;
+import android.telecom.Log;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+public class TestRttActivity extends Activity {
+    private static final String LOG_TAG = TestRttActivity.class.getSimpleName();
+    private static final long NEWLINE_DELAY_MILLIS = 3000;
+
+    private static final int UPDATE_RECEIVED_TEXT = 1;
+    private static final int UPDATE_SENT_TEXT = 2;
+    private static final int RECEIVED_MESSAGE_GAP = 3;
+    private static final int SENT_MESSAGE_GAP = 4;
+
+    private TextView mReceivedText;
+    private TextView mSentText;
+    private EditText mTypingBox;
+
+    private TestCallList mCallList;
+
+    private Handler mTextDisplayHandler = new Handler(Looper.getMainLooper()) {
+        @Override
+        public void handleMessage(Message msg) {
+            String text;
+            switch (msg.what) {
+                case UPDATE_RECEIVED_TEXT:
+                    text = (String) msg.obj;
+                    mReceivedText.append(text);
+                    break;
+                case UPDATE_SENT_TEXT:
+                    text = (String) msg.obj;
+                    mSentText.append(text);
+                    break;
+                case RECEIVED_MESSAGE_GAP:
+                    mReceivedText.append("\n> ");
+                    break;
+                case SENT_MESSAGE_GAP:
+                    mSentText.append("\n> ");
+                    mTypingBox.setText("");
+                    break;
+                default:
+                    Log.w(LOG_TAG, "Invalid message %d", msg.what);
+            }
+        }
+    };
+
+    private Thread mReceiveReader = new Thread() {
+        @Override
+        public void run() {
+            // outer loop
+            while (true) {
+                begin :
+                // sleep and wait if there are no calls
+                while (mCallList.size() > 0) {
+                    Call.RttCall rttCall = mCallList.getCall(0).getRttCall();
+                    if (rttCall == null) {
+                        break;
+                    }
+                    // inner read loop
+                    while (true) {
+                        String receivedText = rttCall.read();
+                        if (receivedText == null) {
+                            if (Thread.currentThread().isInterrupted()) {
+                                break begin;
+                            }
+                            break;
+                        }
+                        Log.d(LOG_TAG, "Received %s", receivedText);
+                        mTextDisplayHandler.removeMessages(RECEIVED_MESSAGE_GAP);
+                        mTextDisplayHandler.sendEmptyMessageDelayed(RECEIVED_MESSAGE_GAP,
+                                NEWLINE_DELAY_MILLIS);
+                        mTextDisplayHandler.obtainMessage(UPDATE_RECEIVED_TEXT, receivedText)
+                                .sendToTarget();
+                    }
+                }
+                if (Thread.currentThread().isInterrupted()) {
+                    break;
+                }
+                try {
+                    Thread.sleep(5000);
+                } catch (InterruptedException e) {
+                    break;
+                }
+            }
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.rtt_incall_screen);
+
+        mReceivedText = (TextView) findViewById(R.id.received_messages_text);
+        mSentText = (TextView) findViewById(R.id.sent_messages_text);
+        mTypingBox = (EditText) findViewById(R.id.rtt_typing_box);
+
+        Button endRttButton = (Button) findViewById(R.id.end_rtt_button);
+        Spinner rttModeSelector = (Spinner) findViewById(R.id.rtt_mode_selection_spinner);
+
+        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
+                R.array.rtt_mode_array, android.R.layout.simple_spinner_item);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        rttModeSelector.setAdapter(adapter);
+
+        mCallList = TestCallList.getInstance();
+        mCallList.addListener(new TestCallList.Listener() {
+            @Override
+            public void onCallRemoved(Call call) {
+                if (mCallList.size() == 0) {
+                    Log.i(LOG_TAG, "Ending the RTT UI");
+                    finish();
+                }
+            }
+        });
+
+        endRttButton.setOnClickListener((view) -> {
+            Call call = mCallList.getCall(0);
+            call.stopRtt();
+        });
+
+        rttModeSelector.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+                CharSequence selection = (CharSequence) parent.getItemAtPosition(position);
+                Call.RttCall call = mCallList.getCall(0).getRttCall();
+                switch (selection.toString()) {
+                    case "Full":
+                        call.setRttMode(Call.RttCall.RTT_MODE_FULL);
+                        break;
+                    case "HCO":
+                        call.setRttMode(Call.RttCall.RTT_MODE_HCO);
+                        break;
+                    case "VCO":
+                        call.setRttMode(Call.RttCall.RTT_MODE_VCO);
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Bad name for rtt mode: %s", selection.toString());
+                }
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+            }
+        });
+
+        mTypingBox.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                if (count == 0 || count < before) {
+                    // ignore deletions and clears
+                    return;
+                }
+                // Only appending at the end is supported.
+                int numCharsInserted = count - before;
+                String toAppend =
+                        s.subSequence(s.length() - numCharsInserted, s.length()).toString();
+
+                if (toAppend.isEmpty()) {
+                    return;
+                }
+                try {
+                    mCallList.getCall(0).getRttCall().write(toAppend);
+                } catch (IOException e) {
+                    Log.w(LOG_TAG, "Exception sending text %s: %s", toAppend, e);
+                }
+                mTextDisplayHandler.removeMessages(SENT_MESSAGE_GAP);
+                mTextDisplayHandler.sendEmptyMessageDelayed(SENT_MESSAGE_GAP, NEWLINE_DELAY_MILLIS);
+                mTextDisplayHandler.obtainMessage(UPDATE_SENT_TEXT, toAppend).sendToTarget();
+            }
+
+            @Override
+            public void afterTextChanged(Editable s) {
+            }
+        });
+
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mReceiveReader.start();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mReceiveReader.interrupt();
+    }
+
+}
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index bf12411..00760fe 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -121,6 +121,10 @@
         }
 
         @Override
+        public void onRttUpgradeRequest(String callId, int id) throws RemoteException {
+        }
+
+        @Override
         public IBinder asBinder() {
             return this;
         }