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