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() {}
 }