| /* |
| * 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(); |
| } |
| } |