Introduce APIs for RTT calls (part 1)
Add signaling methods and data pipes for handling real-time text during
a call.
Change-Id: I876827c448252c5f786d7a4919c47891acb03877
Test: manual, through telecom testapps
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index c69b7c2..d67a0d6 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -16,12 +16,21 @@
package android.telecom;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.lang.String;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -828,6 +837,142 @@
* @param extras Extras associated with the connection event.
*/
public void onConnectionEvent(Call call, String event, Bundle extras) {}
+
+ /**
+ * Invoked when the RTT mode changes for this call.
+ * @param call The call whose RTT mode has changed.
+ * @param mode the new RTT mode, one of
+ * {@link RttCall#RTT_MODE_FULL}, {@link RttCall#RTT_MODE_HCO},
+ * or {@link RttCall#RTT_MODE_VCO}
+ */
+ public void onRttModeChanged(Call call, int mode) {}
+
+ /**
+ * Invoked when the call's RTT status changes, either from off to on or from on to off.
+ * @param call The call whose RTT status has changed.
+ * @param enabled whether RTT is now enabled or disabled
+ * @param rttCall the {@link RttCall} object to use for reading and writing if RTT is now
+ * on, null otherwise.
+ */
+ public void onRttStatusChanged(Call call, boolean enabled, RttCall rttCall) {}
+
+ /**
+ * Invoked when the remote end of the connection has requested that an RTT communication
+ * channel be opened. A response to this should be sent via {@link #respondToRttRequest}
+ * with the same ID that this method is invoked with.
+ * @param call The call which the RTT request was placed on
+ * @param id The ID of the request.
+ */
+ public void onRttRequest(Call call, int id) {}
+ }
+
+ /**
+ * A class that holds the state that describes the state of the RTT channel to the remote
+ * party, if it is active.
+ */
+ public static final class RttCall {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RTT_MODE_INVALID, RTT_MODE_FULL, RTT_MODE_HCO, RTT_MODE_VCO})
+ public @interface RttAudioMode {}
+
+ /**
+ * For metrics use. Default value in the proto.
+ * @hide
+ */
+ public static final int RTT_MODE_INVALID = 0;
+
+ /**
+ * Indicates that there should be a bidirectional audio stream between the two parties
+ * on the call.
+ */
+ public static final int RTT_MODE_FULL = 1;
+
+ /**
+ * Indicates that the local user should be able to hear the audio stream from the remote
+ * user, but not vice versa. Equivalent to muting the microphone.
+ */
+ public static final int RTT_MODE_HCO = 2;
+
+ /**
+ * Indicates that the remote user should be able to hear the audio stream from the local
+ * user, but not vice versa. Equivalent to setting the volume to zero.
+ */
+ public static final int RTT_MODE_VCO = 3;
+
+ private static final int READ_BUFFER_SIZE = 1000;
+
+ private InputStreamReader mReceiveStream;
+ private OutputStreamWriter mTransmitStream;
+ private int mRttMode;
+ private final InCallAdapter mInCallAdapter;
+ private char[] mReadBuffer = new char[READ_BUFFER_SIZE];
+
+ /**
+ * @hide
+ */
+ public RttCall(InputStreamReader receiveStream, OutputStreamWriter transmitStream,
+ int mode, InCallAdapter inCallAdapter) {
+ mReceiveStream = receiveStream;
+ mTransmitStream = transmitStream;
+ mRttMode = mode;
+ mInCallAdapter = inCallAdapter;
+ }
+
+ /**
+ * Returns the current RTT audio mode.
+ * @return Current RTT audio mode. One of {@link #RTT_MODE_FULL}, {@link #RTT_MODE_VCO}, or
+ * {@link #RTT_MODE_HCO}.
+ */
+ public int getRttAudioMode() {
+ return mRttMode;
+ }
+
+ /**
+ * Sets the RTT audio mode. The requested mode change will be communicated through
+ * {@link Callback#onRttModeChanged(Call, int)}.
+ * @param mode The desired RTT audio mode, one of {@link #RTT_MODE_FULL},
+ * {@link #RTT_MODE_VCO}, or {@link #RTT_MODE_HCO}.
+ */
+ public void setRttMode(@RttAudioMode int mode) {
+ mInCallAdapter.setRttMode(mode);
+ }
+
+ /**
+ * Writes the string {@param input} into the outgoing text stream for this RTT call. Since
+ * RTT transmits text in real-time, this method should be called once for each character
+ * the user enters into the device.
+ *
+ * This method is not thread-safe -- calling it from multiple threads simultaneously may
+ * lead to interleaved text.
+ * @param input The message to send to the remote user.
+ */
+ public void write(String input) throws IOException {
+ mTransmitStream.write(input);
+ mTransmitStream.flush();
+ }
+
+ /**
+ * Reads a string from the remote user, blocking if there is no data available. Returns
+ * {@code null} if the RTT conversation has been terminated and there is no further data
+ * to read.
+ *
+ * This method is not thread-safe -- calling it from multiple threads simultaneously may
+ * lead to interleaved text.
+ * @return A string containing text sent by the remote user, or {@code null} if the
+ * conversation has been terminated or if there was an error while reading.
+ */
+ public String read() {
+ try {
+ int numRead = mReceiveStream.read(mReadBuffer, 0, READ_BUFFER_SIZE);
+ if (numRead < 0) {
+ return null;
+ }
+ return new String(mReadBuffer, 0, numRead);
+ } catch (IOException e) {
+ Log.w(this, "Exception encountered when reading from InputStreamReader: %s", e);
+ return null;
+ }
+ }
}
/**
@@ -856,6 +1001,7 @@
private String mCallingPackage;
private String mRemainingPostDialSequence;
private VideoCallImpl mVideoCallImpl;
+ private RttCall mRttCall;
private Details mDetails;
private Bundle mExtras;
@@ -1054,6 +1200,34 @@
}
/**
+ * Sends an RTT upgrade request to the remote end of the connection. Success is not
+ * guaranteed, and notification of success will be via the
+ * {@link Callback#onRttStatusChanged(Call, boolean, RttCall)} callback.
+ */
+ public void sendRttRequest() {
+ mInCallAdapter.sendRttRequest();
+ }
+
+ /**
+ * Responds to an RTT request received via the {@link Callback#onRttRequest(Call, int)} )}
+ * callback.
+ * The ID used here should be the same as the ID that was received via the callback.
+ * @param id The request ID received via {@link Callback#onRttRequest(Call, int)}
+ * @param accept {@code true} if the RTT request should be accepted, {@code false} otherwise.
+ */
+ public void respondToRttRequest(int id, boolean accept) {
+ mInCallAdapter.respondToRttRequest(id, accept);
+ }
+
+ /**
+ * Terminate the RTT session on this call. The resulting state change will be notified via
+ * the {@link Callback#onRttStatusChanged(Call, boolean, RttCall)} callback.
+ */
+ public void stopRtt() {
+ mInCallAdapter.stopRtt();
+ }
+
+ /**
* Adds some extras to this {@link Call}. Existing keys are replaced and new ones are
* added.
* <p>
@@ -1233,6 +1407,23 @@
}
/**
+ * Returns this call's RttCall object. The {@link RttCall} instance is used to send and
+ * receive RTT text data, as well as to change the RTT mode.
+ * @return A {@link Call.RttCall}. {@code null} if there is no active RTT connection.
+ */
+ public @Nullable RttCall getRttCall() {
+ return mRttCall;
+ }
+
+ /**
+ * Returns whether this call has an active RTT connection.
+ * @return true if there is a connection, false otherwise.
+ */
+ public boolean isRttActive() {
+ return mRttCall != null;
+ }
+
+ /**
* Registers a callback to this {@code Call}.
*
* @param callback A {@code Callback}.
@@ -1426,6 +1617,32 @@
fireConferenceableCallsChanged();
}
+ boolean isRttChanged = false;
+ boolean rttModeChanged = false;
+ if (parcelableCall.getParcelableRttCall() != null && parcelableCall.getIsRttCallChanged()) {
+ ParcelableRttCall parcelableRttCall = parcelableCall.getParcelableRttCall();
+ InputStreamReader receiveStream = new InputStreamReader(
+ new ParcelFileDescriptor.AutoCloseInputStream(
+ parcelableRttCall.getReceiveStream()),
+ StandardCharsets.UTF_8);
+ OutputStreamWriter transmitStream = new OutputStreamWriter(
+ new ParcelFileDescriptor.AutoCloseOutputStream(
+ parcelableRttCall.getTransmitStream()),
+ StandardCharsets.UTF_8);
+ RttCall newRttCall = new Call.RttCall(
+ receiveStream, transmitStream, parcelableRttCall.getRttMode(), mInCallAdapter);
+ if (mRttCall == null) {
+ isRttChanged = true;
+ } else if (mRttCall.getRttAudioMode() != newRttCall.getRttAudioMode()) {
+ rttModeChanged = true;
+ }
+ mRttCall = newRttCall;
+ } else if (mRttCall != null && parcelableCall.getParcelableRttCall() == null
+ && parcelableCall.getIsRttCallChanged()) {
+ isRttChanged = true;
+ mRttCall = null;
+ }
+
// Now we fire updates, ensuring that any client who listens to any of these notifications
// gets the most up-to-date state.
@@ -1447,6 +1664,12 @@
if (childrenChanged) {
fireChildrenChanged(getChildren());
}
+ if (isRttChanged) {
+ fireOnIsRttChanged(mRttCall != null, mRttCall);
+ }
+ if (rttModeChanged) {
+ fireOnRttModeChanged(mRttCall.getRttAudioMode());
+ }
// If we have transitioned to DISCONNECTED, that means we need to notify clients and
// remove ourselves from the Phone. Note that we do this after completing all state updates
@@ -1477,6 +1700,15 @@
fireOnConnectionEvent(event, extras);
}
+ /** {@hide} */
+ final void internalOnRttUpgradeRequest(final int requestId) {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onRttRequest(call, requestId));
+ }
+ }
+
private void fireStateChanged(final int newState) {
for (CallbackRecord<Callback> record : mCallbackRecords) {
final Call call = this;
@@ -1645,6 +1877,32 @@
}
/**
+ * Notifies listeners of an RTT on/off change
+ *
+ * @param enabled True if RTT is now enabled, false otherwise
+ */
+ private void fireOnIsRttChanged(final boolean enabled, final RttCall rttCall) {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onRttStatusChanged(call, enabled, rttCall));
+ }
+ }
+
+ /**
+ * Notifies listeners of a RTT mode change
+ *
+ * @param mode The new RTT mode
+ */
+ private void fireOnRttModeChanged(final int mode) {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onRttModeChanged(call, mode));
+ }
+ }
+
+ /**
* Determines if two bundles are equal.
*
* @param bundle The original bundle.