/* * 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.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 (IOException e) { Rlog.e(LOG_TAG, "RttReaderThread - IOException encountered " + "reading from in-call: %s", e); obtainMessage(TEARDOWN).sendToTarget(); break; } if (charsReceived == null) { if (Thread.currentThread().isInterrupted()) { Rlog.i(LOG_TAG, "RttReaderThread - Thread interrupted. Finishing."); break; } 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) { sendMessageAtFrontOfQueue(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) { sendMessageAtFrontOfQueue(obtainMessage(ATTEMPT_SEND_TO_NETWORK)); } break; case TEARDOWN: try { if (mReaderThread != null) { 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) { obtainMessage(INITIALIZE, rttTextStream).sendToTarget(); } public void tearDown() { obtainMessage(TEARDOWN).sendToTarget(); } @VisibleForTesting public void setReadNotifier(CountDownLatch latch) { mReadNotifier = latch; } public String getNetworkBufferText() { return mBufferedTextToNetwork.toString(); } }