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 import android.telephony.Rlog;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 import java.io.IOException;
28 import java.util.concurrent.CountDownLatch;
29 
30 public class ImsRttTextHandler extends Handler {
31     public interface NetworkWriter {
write(String s)32         void write(String s);
33     }
34 
35     private static final String LOG_TAG = "ImsRttTextHandler";
36     // RTT buffering and sending tuning constants.
37     // TODO: put this in carrier config?
38 
39     // These count Unicode codepoints, not Java char types.
40     public static final int MAX_CODEPOINTS_PER_SECOND = 30;
41     // Assuming that we do not exceed the rate limit, this is the maximum time between when a
42     // piece of text is received and when it is actually sent over the network.
43     public static final int MAX_BUFFERING_DELAY_MILLIS = 200;
44     // Assuming that we do not exceed the rate limit, this is the maximum size we will allow
45     // the buffer to grow to before sending as many as we can.
46     public static final int MAX_BUFFERED_CHARACTER_COUNT = 5;
47     private static final int MILLIS_PER_SECOND = 1000;
48 
49     // Messages for the handler.
50     // Initializes the text handler. Should have an RttTextStream set in msg.obj
51     private static final int INITIALIZE = 1;
52     // Appends a string to the buffer to send to the network. Should have the string in msg.obj
53     private static final int APPEND_TO_NETWORK_BUFFER = 2;
54     // Send a string received from the network to the in-call app. Should have the string in
55     // msg.obj.
56     private static final int SEND_TO_INCALL = 3;
57     // Send as many characters as possible, as constrained by the rate limit. No extra data.
58     private static final int ATTEMPT_SEND_TO_NETWORK = 4;
59     // Indicates that N characters were sent a second ago and should be ignored by the rate
60     // limiter. msg.arg1 should be set to N.
61     private static final int EXPIRE_SENT_CODEPOINT_COUNT = 5;
62     // Indicates that the call is over and we should teardown everything we have set up.
63     private static final int TEARDOWN = 9999;
64 
65     private Connection.RttTextStream mRttTextStream;
66     // For synchronization during testing
67     private CountDownLatch mReadNotifier;
68 
69     private class InCallReaderThread extends Thread {
70         private final Connection.RttTextStream mReaderThreadRttTextStream;
71 
InCallReaderThread(Connection.RttTextStream textStream)72         public InCallReaderThread(Connection.RttTextStream textStream) {
73             mReaderThreadRttTextStream = textStream;
74         }
75 
76         @Override
run()77         public void run() {
78             while (true) {
79                 String charsReceived;
80                 try {
81                     charsReceived = mReaderThreadRttTextStream.read();
82                 } catch (IOException e) {
83                     Rlog.e(LOG_TAG, "RttReaderThread - IOException encountered " +
84                             "reading from in-call: %s", e);
85                     obtainMessage(TEARDOWN).sendToTarget();
86                     break;
87                 }
88                 if (charsReceived == null) {
89                     if (Thread.currentThread().isInterrupted()) {
90                         Rlog.i(LOG_TAG, "RttReaderThread - Thread interrupted. Finishing.");
91                         break;
92                     }
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                 String messageToIncall = (String) msg.obj;
132                 try {
133                     mRttTextStream.write(messageToIncall);
134                 } catch (IOException e) {
135                     Rlog.e(LOG_TAG, "IOException encountered writing to in-call: %s", e);
136                     obtainMessage(TEARDOWN).sendToTarget();
137                     mBufferedTextToIncall.append(messageToIncall);
138                 }
139                 break;
140             case APPEND_TO_NETWORK_BUFFER:
141                 // First, append the text-to-send to the string buffer
142                 mBufferedTextToNetwork.append((String) msg.obj);
143                 // Check to see how many codepoints we have buffered. If we have more than 5,
144                 // send immediately, otherwise, wait until a timeout happens.
145                 int numCodepointsBuffered = mBufferedTextToNetwork
146                         .codePointCount(0, mBufferedTextToNetwork.length());
147                 if (numCodepointsBuffered >= MAX_BUFFERED_CHARACTER_COUNT) {
148                     sendMessageAtFrontOfQueue(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
149                 } else {
150                     sendEmptyMessageDelayed(
151                             ATTEMPT_SEND_TO_NETWORK, MAX_BUFFERING_DELAY_MILLIS);
152                 }
153                 break;
154             case ATTEMPT_SEND_TO_NETWORK:
155                 // Check to see how many codepoints we can send, and send that many.
156                 int numCodePointsAvailableInBuffer = mBufferedTextToNetwork.codePointCount(0,
157                         mBufferedTextToNetwork.length());
158                 int numCodePointsSent = Math.min(numCodePointsAvailableInBuffer,
159                         mCodepointsAvailableForTransmission);
160                 if (numCodePointsSent == 0) {
161                     break;
162                 }
163                 int endSendIndex = mBufferedTextToNetwork.offsetByCodePoints(0,
164                         numCodePointsSent);
165 
166                 String stringToSend = mBufferedTextToNetwork.substring(0, endSendIndex);
167 
168                 mBufferedTextToNetwork.delete(0, endSendIndex);
169                 mNetworkWriter.write(stringToSend);
170                 mCodepointsAvailableForTransmission -= numCodePointsSent;
171                 sendMessageDelayed(
172                         obtainMessage(EXPIRE_SENT_CODEPOINT_COUNT, numCodePointsSent, 0),
173                         MILLIS_PER_SECOND);
174                 break;
175             case EXPIRE_SENT_CODEPOINT_COUNT:
176                 mCodepointsAvailableForTransmission += msg.arg1;
177                 if (mCodepointsAvailableForTransmission > 0) {
178                     sendMessageAtFrontOfQueue(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
179                 }
180                 break;
181             case TEARDOWN:
182                 try {
183                     if (mReaderThread != null) {
184                         mReaderThread.join(1000);
185                     }
186                 } catch (InterruptedException e) {
187                     // Ignore and assume it'll finish on its own.
188                 }
189                 mReaderThread = null;
190                 mRttTextStream = null;
191                 break;
192         }
193     }
194 
ImsRttTextHandler(Looper looper, NetworkWriter networkWriter)195     public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
196         super(looper);
197         mNetworkWriter = networkWriter;
198     }
199 
sendToInCall(String msg)200     public void sendToInCall(String msg) {
201         obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
202     }
203 
initialize(Connection.RttTextStream rttTextStream)204     public void initialize(Connection.RttTextStream rttTextStream) {
205         obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
206     }
207 
tearDown()208     public void tearDown() {
209         obtainMessage(TEARDOWN).sendToTarget();
210     }
211 
212     @VisibleForTesting
setReadNotifier(CountDownLatch latch)213     public void setReadNotifier(CountDownLatch latch) {
214         mReadNotifier = latch;
215     }
216 
getNetworkBufferText()217     public String getNetworkBufferText() {
218         return mBufferedTextToNetwork.toString();
219     }
220 }
221