1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.internal.telephony.imsphone;
18 
19 import android.os.Handler;
20 import android.os.Looper;
21 import android.os.Message;
22 import android.telecom.Connection;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.telephony.Rlog;
26 
27 import java.io.IOException;
28 import java.nio.channels.ClosedByInterruptException;
29 import java.util.concurrent.CountDownLatch;
30 
31 public class ImsRttTextHandler extends Handler {
32     public interface NetworkWriter {
write(String s)33         void write(String s);
34     }
35 
36     private static final String LOG_TAG = "ImsRttTextHandler";
37     // RTT buffering and sending tuning constants.
38     // TODO: put this in carrier config?
39 
40     // These count Unicode codepoints, not Java char types.
41     public static final int MAX_CODEPOINTS_PER_SECOND = 30;
42     // Assuming that we do not exceed the rate limit, this is the maximum time between when a
43     // piece of text is received and when it is actually sent over the network.
44     public static final int MAX_BUFFERING_DELAY_MILLIS = 200;
45     // Assuming that we do not exceed the rate limit, this is the maximum size we will allow
46     // the buffer to grow to before sending as many as we can.
47     public static final int MAX_BUFFERED_CHARACTER_COUNT = 5;
48     private static final int MILLIS_PER_SECOND = 1000;
49 
50     // Messages for the handler.
51     // Initializes the text handler. Should have an RttTextStream set in msg.obj
52     private static final int INITIALIZE = 1;
53     // Appends a string to the buffer to send to the network. Should have the string in msg.obj
54     private static final int APPEND_TO_NETWORK_BUFFER = 2;
55     // Send a string received from the network to the in-call app. Should have the string in
56     // msg.obj.
57     private static final int SEND_TO_INCALL = 3;
58     // Send as many characters as possible, as constrained by the rate limit. No extra data.
59     private static final int ATTEMPT_SEND_TO_NETWORK = 4;
60     // Indicates that N characters were sent a second ago and should be ignored by the rate
61     // limiter. msg.arg1 should be set to N.
62     private static final int EXPIRE_SENT_CODEPOINT_COUNT = 5;
63     // Indicates that the call is over and we should teardown everything we have set up.
64     private static final int TEARDOWN = 9999;
65 
66     private Connection.RttTextStream mRttTextStream;
67     // For synchronization during testing
68     private CountDownLatch mReadNotifier;
69 
70     private class InCallReaderThread extends Thread {
71         private final Connection.RttTextStream mReaderThreadRttTextStream;
72 
InCallReaderThread(Connection.RttTextStream textStream)73         public InCallReaderThread(Connection.RttTextStream textStream) {
74             mReaderThreadRttTextStream = textStream;
75         }
76 
77         @Override
run()78         public void run() {
79             while (true) {
80                 String charsReceived;
81                 try {
82                     charsReceived = mReaderThreadRttTextStream.read();
83                 } catch (ClosedByInterruptException e) {
84                     Rlog.i(LOG_TAG, "RttReaderThread - Thread interrupted. Finishing.");
85                     break;
86                 } catch (IOException e) {
87                     Rlog.e(LOG_TAG, "RttReaderThread - IOException encountered " +
88                             "reading from in-call: ", e);
89                     obtainMessage(TEARDOWN).sendToTarget();
90                     break;
91                 }
92                 if (charsReceived == null) {
93                     Rlog.e(LOG_TAG, "RttReaderThread - Stream closed unexpectedly. Attempt to " +
94                             "reinitialize.");
95                     obtainMessage(TEARDOWN).sendToTarget();
96                     break;
97                 }
98                 if (charsReceived.length() == 0) {
99                     continue;
100                 }
101                 obtainMessage(APPEND_TO_NETWORK_BUFFER, charsReceived)
102                         .sendToTarget();
103                 if (mReadNotifier != null) {
104                     mReadNotifier.countDown();
105                 }
106             }
107         }
108     }
109 
110     private int mCodepointsAvailableForTransmission = MAX_CODEPOINTS_PER_SECOND;
111     private StringBuffer mBufferedTextToNetwork = new StringBuffer();
112     private InCallReaderThread mReaderThread;
113     // This is only ever used when the pipes fail and we have to re-setup. Messages received
114     // from the network are buffered here until Telecom gets back to us with the new pipes.
115     private StringBuffer mBufferedTextToIncall = new StringBuffer();
116     private final NetworkWriter mNetworkWriter;
117 
118     @Override
handleMessage(Message msg)119     public void handleMessage(Message msg) {
120         switch (msg.what) {
121             case INITIALIZE:
122                 if (mRttTextStream != null || mReaderThread != null) {
123                     Rlog.e(LOG_TAG, "RTT text stream already initialized. Ignoring.");
124                     return;
125                 }
126                 mRttTextStream = (Connection.RttTextStream) msg.obj;
127                 mReaderThread = new InCallReaderThread(mRttTextStream);
128                 mReaderThread.start();
129                 break;
130             case SEND_TO_INCALL:
131                 if (msg.obj == null) {
132                     Rlog.e(LOG_TAG, "RTT msg.obj is null. Ignoring.");
133                     return;
134                 }
135                 String messageToIncall = (String) msg.obj;
136                 if (mRttTextStream == null) {
137                     Rlog.e(LOG_TAG, "RTT text stream is null. Writing to in-call buffer.");
138                     mBufferedTextToIncall.append(messageToIncall);
139                     return;
140                 }
141                 try {
142                     mRttTextStream.write(messageToIncall);
143                 } catch (IOException e) {
144                     Rlog.e(LOG_TAG, "IOException encountered writing to in-call: %s", e);
145                     obtainMessage(TEARDOWN).sendToTarget();
146                     mBufferedTextToIncall.append(messageToIncall);
147                 }
148                 break;
149             case APPEND_TO_NETWORK_BUFFER:
150                 // First, append the text-to-send to the string buffer
151                 mBufferedTextToNetwork.append((String) msg.obj);
152                 // Check to see how many codepoints we have buffered. If we have more than 5,
153                 // send immediately, otherwise, wait until a timeout happens.
154                 int numCodepointsBuffered = mBufferedTextToNetwork
155                         .codePointCount(0, mBufferedTextToNetwork.length());
156                 if (numCodepointsBuffered >= MAX_BUFFERED_CHARACTER_COUNT) {
157                     sendMessage(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
158                 } else {
159                     sendEmptyMessageDelayed(
160                             ATTEMPT_SEND_TO_NETWORK, MAX_BUFFERING_DELAY_MILLIS);
161                 }
162                 break;
163             case ATTEMPT_SEND_TO_NETWORK:
164                 // Check to see how many codepoints we can send, and send that many.
165                 int numCodePointsAvailableInBuffer = mBufferedTextToNetwork.codePointCount(0,
166                         mBufferedTextToNetwork.length());
167                 int numCodePointsSent = Math.min(numCodePointsAvailableInBuffer,
168                         mCodepointsAvailableForTransmission);
169                 if (numCodePointsSent == 0) {
170                     break;
171                 }
172                 int endSendIndex = mBufferedTextToNetwork.offsetByCodePoints(0,
173                         numCodePointsSent);
174 
175                 String stringToSend = mBufferedTextToNetwork.substring(0, endSendIndex);
176 
177                 mBufferedTextToNetwork.delete(0, endSendIndex);
178                 mNetworkWriter.write(stringToSend);
179                 mCodepointsAvailableForTransmission -= numCodePointsSent;
180                 sendMessageDelayed(
181                         obtainMessage(EXPIRE_SENT_CODEPOINT_COUNT, numCodePointsSent, 0),
182                         MILLIS_PER_SECOND);
183                 break;
184             case EXPIRE_SENT_CODEPOINT_COUNT:
185                 mCodepointsAvailableForTransmission += msg.arg1;
186                 if (mCodepointsAvailableForTransmission > 0) {
187                     sendMessage(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
188                 }
189                 break;
190             case TEARDOWN:
191                 try {
192                     if (mReaderThread != null) {
193                         mReaderThread.interrupt();
194                         mReaderThread.join(1000);
195                     }
196                 } catch (InterruptedException e) {
197                     // Ignore and assume it'll finish on its own.
198                 }
199                 mReaderThread = null;
200                 mRttTextStream = null;
201                 break;
202         }
203     }
204 
ImsRttTextHandler(Looper looper, NetworkWriter networkWriter)205     public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
206         super(looper);
207         mNetworkWriter = networkWriter;
208     }
209 
sendToInCall(String msg)210     public void sendToInCall(String msg) {
211         obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
212     }
213 
initialize(Connection.RttTextStream rttTextStream)214     public void initialize(Connection.RttTextStream rttTextStream) {
215         Rlog.i(LOG_TAG, "Initializing: " + this);
216         obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
217     }
218 
tearDown()219     public void tearDown() {
220         obtainMessage(TEARDOWN).sendToTarget();
221     }
222 
223     @VisibleForTesting
setReadNotifier(CountDownLatch latch)224     public void setReadNotifier(CountDownLatch latch) {
225         mReadNotifier = latch;
226     }
227 
228     @VisibleForTesting
getBufferedTextToIncall()229     public StringBuffer getBufferedTextToIncall() {
230         return mBufferedTextToIncall;
231     }
232 
233     @VisibleForTesting
setRttTextStream(Connection.RttTextStream rttTextStream)234     public void setRttTextStream(Connection.RttTextStream rttTextStream) {
235         mRttTextStream = rttTextStream;
236     }
237 
238     @VisibleForTesting
getSendToIncall()239     public int getSendToIncall() {
240         return SEND_TO_INCALL;
241     }
242 
getNetworkBufferText()243     public String getNetworkBufferText() {
244         return mBufferedTextToNetwork.toString();
245     }
246 }
247