Merge "Add unit tests for some VideoProfile static methods."
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index 2173fa5..0ad5d4c 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -630,7 +630,12 @@
         } else {
             addressUri = call.getHandle();
         }
+
         String address = addressUri == null ? null : addressUri.getSchemeSpecificPart();
+        if (address != null) {
+            address = PhoneNumberUtils.stripSeparators(address);
+        }
+
         int addressType = address == null ? -1 : PhoneNumberUtils.toaFromString(address);
 
         if (shouldLog) {
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 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..df1bb4a 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(extras)
+                        .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;
         }