blob: eff2499792dbbebcd35b91132e17a4ca57fec055 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.telephony.imsphone;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.telecom.Connection;
import android.telephony.Rlog;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
import java.util.concurrent.CountDownLatch;
public class ImsRttTextHandler extends Handler {
public interface NetworkWriter {
void write(String s);
}
private static final String LOG_TAG = "ImsRttTextHandler";
// RTT buffering and sending tuning constants.
// TODO: put this in carrier config?
// These count Unicode codepoints, not Java char types.
public static final int MAX_CODEPOINTS_PER_SECOND = 30;
// Assuming that we do not exceed the rate limit, this is the maximum time between when a
// piece of text is received and when it is actually sent over the network.
public static final int MAX_BUFFERING_DELAY_MILLIS = 200;
// Assuming that we do not exceed the rate limit, this is the maximum size we will allow
// the buffer to grow to before sending as many as we can.
public static final int MAX_BUFFERED_CHARACTER_COUNT = 5;
private static final int MILLIS_PER_SECOND = 1000;
// Messages for the handler.
// Initializes the text handler. Should have an RttTextStream set in msg.obj
private static final int INITIALIZE = 1;
// Appends a string to the buffer to send to the network. Should have the string in msg.obj
private static final int APPEND_TO_NETWORK_BUFFER = 2;
// Send a string received from the network to the in-call app. Should have the string in
// msg.obj.
private static final int SEND_TO_INCALL = 3;
// Send as many characters as possible, as constrained by the rate limit. No extra data.
private static final int ATTEMPT_SEND_TO_NETWORK = 4;
// Indicates that N characters were sent a second ago and should be ignored by the rate
// limiter. msg.arg1 should be set to N.
private static final int EXPIRE_SENT_CODEPOINT_COUNT = 5;
// Indicates that the call is over and we should teardown everything we have set up.
private static final int TEARDOWN = 9999;
private Connection.RttTextStream mRttTextStream;
// For synchronization during testing
private CountDownLatch mReadNotifier;
private class InCallReaderThread extends Thread {
private final Connection.RttTextStream mReaderThreadRttTextStream;
public InCallReaderThread(Connection.RttTextStream textStream) {
mReaderThreadRttTextStream = textStream;
}
@Override
public void run() {
while (true) {
String charsReceived;
try {
charsReceived = mReaderThreadRttTextStream.read();
} catch (ClosedByInterruptException e) {
Rlog.i(LOG_TAG, "RttReaderThread - Thread interrupted. Finishing.");
break;
} catch (IOException e) {
Rlog.e(LOG_TAG, "RttReaderThread - IOException encountered " +
"reading from in-call: ", e);
obtainMessage(TEARDOWN).sendToTarget();
break;
}
if (charsReceived == null) {
Rlog.e(LOG_TAG, "RttReaderThread - Stream closed unexpectedly. Attempt to " +
"reinitialize.");
obtainMessage(TEARDOWN).sendToTarget();
break;
}
if (charsReceived.length() == 0) {
continue;
}
obtainMessage(APPEND_TO_NETWORK_BUFFER, charsReceived)
.sendToTarget();
if (mReadNotifier != null) {
mReadNotifier.countDown();
}
}
}
}
private int mCodepointsAvailableForTransmission = MAX_CODEPOINTS_PER_SECOND;
private StringBuffer mBufferedTextToNetwork = new StringBuffer();
private InCallReaderThread mReaderThread;
// This is only ever used when the pipes fail and we have to re-setup. Messages received
// from the network are buffered here until Telecom gets back to us with the new pipes.
private StringBuffer mBufferedTextToIncall = new StringBuffer();
private final NetworkWriter mNetworkWriter;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case INITIALIZE:
if (mRttTextStream != null || mReaderThread != null) {
Rlog.e(LOG_TAG, "RTT text stream already initialized. Ignoring.");
return;
}
mRttTextStream = (Connection.RttTextStream) msg.obj;
mReaderThread = new InCallReaderThread(mRttTextStream);
mReaderThread.start();
break;
case SEND_TO_INCALL:
String messageToIncall = (String) msg.obj;
try {
mRttTextStream.write(messageToIncall);
} catch (IOException e) {
Rlog.e(LOG_TAG, "IOException encountered writing to in-call: %s", e);
obtainMessage(TEARDOWN).sendToTarget();
mBufferedTextToIncall.append(messageToIncall);
}
break;
case APPEND_TO_NETWORK_BUFFER:
// First, append the text-to-send to the string buffer
mBufferedTextToNetwork.append((String) msg.obj);
// Check to see how many codepoints we have buffered. If we have more than 5,
// send immediately, otherwise, wait until a timeout happens.
int numCodepointsBuffered = mBufferedTextToNetwork
.codePointCount(0, mBufferedTextToNetwork.length());
if (numCodepointsBuffered >= MAX_BUFFERED_CHARACTER_COUNT) {
sendMessage(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
} else {
sendEmptyMessageDelayed(
ATTEMPT_SEND_TO_NETWORK, MAX_BUFFERING_DELAY_MILLIS);
}
break;
case ATTEMPT_SEND_TO_NETWORK:
// Check to see how many codepoints we can send, and send that many.
int numCodePointsAvailableInBuffer = mBufferedTextToNetwork.codePointCount(0,
mBufferedTextToNetwork.length());
int numCodePointsSent = Math.min(numCodePointsAvailableInBuffer,
mCodepointsAvailableForTransmission);
if (numCodePointsSent == 0) {
break;
}
int endSendIndex = mBufferedTextToNetwork.offsetByCodePoints(0,
numCodePointsSent);
String stringToSend = mBufferedTextToNetwork.substring(0, endSendIndex);
mBufferedTextToNetwork.delete(0, endSendIndex);
mNetworkWriter.write(stringToSend);
mCodepointsAvailableForTransmission -= numCodePointsSent;
sendMessageDelayed(
obtainMessage(EXPIRE_SENT_CODEPOINT_COUNT, numCodePointsSent, 0),
MILLIS_PER_SECOND);
break;
case EXPIRE_SENT_CODEPOINT_COUNT:
mCodepointsAvailableForTransmission += msg.arg1;
if (mCodepointsAvailableForTransmission > 0) {
sendMessage(obtainMessage(ATTEMPT_SEND_TO_NETWORK));
}
break;
case TEARDOWN:
try {
if (mReaderThread != null) {
mReaderThread.interrupt();
mReaderThread.join(1000);
}
} catch (InterruptedException e) {
// Ignore and assume it'll finish on its own.
}
mReaderThread = null;
mRttTextStream = null;
break;
}
}
public ImsRttTextHandler(Looper looper, NetworkWriter networkWriter) {
super(looper);
mNetworkWriter = networkWriter;
}
public void sendToInCall(String msg) {
obtainMessage(SEND_TO_INCALL, msg).sendToTarget();
}
public void initialize(Connection.RttTextStream rttTextStream) {
Rlog.i(LOG_TAG, "Initializing: " + this);
obtainMessage(INITIALIZE, rttTextStream).sendToTarget();
}
public void tearDown() {
obtainMessage(TEARDOWN).sendToTarget();
}
@VisibleForTesting
public void setReadNotifier(CountDownLatch latch) {
mReadNotifier = latch;
}
public String getNetworkBufferText() {
return mBufferedTextToNetwork.toString();
}
}