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
17package com.android.internal.telephony.imsphone;
18
19import android.os.Handler;
20import android.os.Looper;
21import android.os.Message;
22import android.telecom.Connection;
23import android.telephony.Rlog;
24
25import com.android.internal.annotations.VisibleForTesting;
26
27import java.io.IOException;
28import java.util.concurrent.CountDownLatch;
29
30public class ImsRttTextHandler extends Handler {
31    public interface NetworkWriter {
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
72        public InCallReaderThread(Connection.RttTextStream textStream) {
73            mReaderThreadRttTextStream = textStream;
74        }
75
76        @Override
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
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
195    public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
196        super(looper);
197        mNetworkWriter = networkWriter;
198    }
199
200    public void sendToInCall(String msg) {
201        obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
202    }
203
204    public void initialize(Connection.RttTextStream rttTextStream) {
205        obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
206    }
207
208    public void tearDown() {
209        obtainMessage(TEARDOWN).sendToTarget();
210    }
211
212    @VisibleForTesting
213    public void setReadNotifier(CountDownLatch latch) {
214        mReadNotifier = latch;
215    }
216
217    public String getNetworkBufferText() {
218        return mBufferedTextToNetwork.toString();
219    }
220}
221