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