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                 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                     sendMessage(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                     sendMessage(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
179                 }
180                 break;
181             case TEARDOWN:
182                 try {
183                     if (mReaderThread != null) {
184                         mReaderThread.interrupt();
185                         mReaderThread.join(1000);
186                     }
187                 } catch (InterruptedException e) {
188                     // Ignore and assume it'll finish on its own.
189                 }
190                 mReaderThread = null;
191                 mRttTextStream = null;
192                 break;
193         }
194     }
195 
ImsRttTextHandler(Looper looper, NetworkWriter networkWriter)196     public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
197         super(looper);
198         mNetworkWriter = networkWriter;
199     }
200 
sendToInCall(String msg)201     public void sendToInCall(String msg) {
202         obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
203     }
204 
initialize(Connection.RttTextStream rttTextStream)205     public void initialize(Connection.RttTextStream rttTextStream) {
206         Rlog.i(LOG_TAG, "Initializing: " + this);
207         obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
208     }
209 
tearDown()210     public void tearDown() {
211         obtainMessage(TEARDOWN).sendToTarget();
212     }
213 
214     @VisibleForTesting
setReadNotifier(CountDownLatch latch)215     public void setReadNotifier(CountDownLatch latch) {
216         mReadNotifier = latch;
217     }
218 
getNetworkBufferText()219     public String getNetworkBufferText() {
220         return mBufferedTextToNetwork.toString();
221     }
222 }
223