content: /*
   * Copyright (C) 2013 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.annotation.UnsupportedAppUsage;
  import android.telecom.ConferenceParticipant;
  import android.telephony.Rlog;
  import android.telephony.DisconnectCause;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.Call;
  import com.android.internal.telephony.CallStateException;
  import com.android.internal.telephony.Connection;
  import com.android.internal.telephony.Phone;
  import com.android.ims.ImsCall;
  import com.android.ims.ImsException;
  import android.telephony.ims.ImsStreamMediaProfile;
  
  import java.util.List;
  
  /**
   * {@hide}
   */
  public class ImsPhoneCall extends Call {
      private static final String LOG_TAG = "ImsPhoneCall";
  
      // This flag is meant to be used as a debugging tool to quickly see all logs
      // regardless of the actual log level set on this component.
      private static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */
      private static final boolean DBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.DEBUG);
      private static final boolean VDBG = FORCE_DEBUG || Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
  
      /*************************** Instance Variables **************************/
      public static final String CONTEXT_UNKNOWN = "UK";
      public static final String CONTEXT_RINGING = "RG";
      public static final String CONTEXT_FOREGROUND = "FG";
      public static final String CONTEXT_BACKGROUND = "BG";
      public static final String CONTEXT_HANDOVER = "HO";
  
      /*package*/ ImsPhoneCallTracker mOwner;
  
      private boolean mIsRingbackTonePlaying = false;
  
      // Determines what type of ImsPhoneCall this is.  ImsPhoneCallTracker uses instances of
      // ImsPhoneCall to for fg, bg, etc calls.  This is used as a convenience for logging so that it
      // can be made clear whether a call being logged is the foreground, background, etc.
      private final String mCallContext;
  
      /****************************** Constructors *****************************/
      /*package*/
      ImsPhoneCall() {
          mCallContext = CONTEXT_UNKNOWN;
      }
  
      public ImsPhoneCall(ImsPhoneCallTracker owner, String context) {
          mOwner = owner;
          mCallContext = context;
      }
  
      public void dispose() {
          try {
              mOwner.hangup(this);
          } catch (CallStateException ex) {
              //Rlog.e(LOG_TAG, "dispose: unexpected error on hangup", ex);
              //while disposing, ignore the exception and clean the connections
          } finally {
              for(int i = 0, s = mConnections.size(); i < s; i++) {
                  ImsPhoneConnection c = (ImsPhoneConnection) mConnections.get(i);
                  c.onDisconnect(DisconnectCause.LOST_SIGNAL);
              }
          }
      }
  
      /************************** Overridden from Call *************************/
  
      @UnsupportedAppUsage
      @Override
      public List<Connection>
      getConnections() {
          return mConnections;
      }
  
      @Override
      public Phone
      getPhone() {
          return mOwner.getPhone();
      }
  
      @Override
      public boolean
      isMultiparty() {
          ImsCall imsCall = getImsCall();
          if (imsCall == null) {
              return false;
          }
  
          return imsCall.isMultiparty();
      }
  
      /** Please note: if this is the foreground call and a
       *  background call exists, the background call will be resumed.
       */
      @UnsupportedAppUsage
      @Override
      public void
      hangup() throws CallStateException {
          mOwner.hangup(this);
      }
  
      @Override
      public String toString() {
          StringBuilder sb = new StringBuilder();
          sb.append("[ImsPhoneCall ");
          sb.append(mCallContext);
          sb.append(" state: ");
          sb.append(mState.toString());
          sb.append(" ");
          if (mConnections.size() > 1) {
              sb.append(" ERROR_MULTIPLE ");
          }
          for (Connection conn : mConnections) {
              sb.append(conn);
              sb.append(" ");
          }
  
          sb.append("]");
          return sb.toString();
      }
  
      @Override
      public List<ConferenceParticipant> getConferenceParticipants() {
           ImsCall call = getImsCall();
           if (call == null) {
               return null;
           }
           return call.getConferenceParticipants();
      }
  
      //***** Called from ImsPhoneConnection
  
      public void attach(Connection conn) {
          if (VDBG) {
              Rlog.v(LOG_TAG, "attach : " + mCallContext + " conn = " + conn);
          }
          clearDisconnected();
          mConnections.add(conn);
  
          mOwner.logState();
      }
  
      @UnsupportedAppUsage
      public void attach(Connection conn, State state) {
          if (VDBG) {
              Rlog.v(LOG_TAG, "attach : " + mCallContext + " state = " +
                      state.toString());
          }
          this.attach(conn);
          mState = state;
      }
  
      @UnsupportedAppUsage
      public void attachFake(Connection conn, State state) {
          attach(conn, state);
      }
  
      /**
       * Called by ImsPhoneConnection when it has disconnected
       */
      public boolean connectionDisconnected(ImsPhoneConnection conn) {
          if (mState != State.DISCONNECTED) {
              /* If only disconnected connections remain, we are disconnected*/
  
              boolean hasOnlyDisconnectedConnections = true;
  
              for (int i = 0, s = mConnections.size()  ; i < s; i ++) {
                  if (mConnections.get(i).getState() != State.DISCONNECTED) {
                      hasOnlyDisconnectedConnections = false;
                      break;
                  }
              }
  
              if (hasOnlyDisconnectedConnections) {
                  mState = State.DISCONNECTED;
                  if (VDBG) {
                      Rlog.v(LOG_TAG, "connectionDisconnected : " + mCallContext + " state = " +
                              mState);
                  }
                  return true;
              }
          }
  
          return false;
      }
  
      public void detach(ImsPhoneConnection conn) {
          if (VDBG) {
              Rlog.v(LOG_TAG, "detach : " + mCallContext + " conn = " + conn);
          }
          mConnections.remove(conn);
          clearDisconnected();
  
          mOwner.logState();
      }
  
      /**
       * @return true if there's no space in this call for additional
       * connections to be added via "conference"
       */
      /*package*/ boolean
      isFull() {
          return mConnections.size() == ImsPhoneCallTracker.MAX_CONNECTIONS_PER_CALL;
      }
  
      //***** Called from ImsPhoneCallTracker
      /**
       * Called when this Call is being hung up locally (eg, user pressed "end")
       */
      @UnsupportedAppUsage
      void
      onHangupLocal() {
          for (int i = 0, s = mConnections.size(); i < s; i++) {
              ImsPhoneConnection cn = (ImsPhoneConnection)mConnections.get(i);
              cn.onHangupLocal();
          }
          mState = State.DISCONNECTING;
          if (VDBG) {
              Rlog.v(LOG_TAG, "onHangupLocal : " + mCallContext + " state = " + mState);
          }
      }
  
      @VisibleForTesting
      public ImsPhoneConnection getFirstConnection() {
          if (mConnections.size() == 0) return null;
  
          return (ImsPhoneConnection) mConnections.get(0);
      }
  
      /*package*/ void
      setMute(boolean mute) {
          ImsCall imsCall = getFirstConnection() == null ?
                  null : getFirstConnection().getImsCall();
          if (imsCall != null) {
              try {
                  imsCall.setMute(mute);
              } catch (ImsException e) {
                  Rlog.e(LOG_TAG, "setMute failed : " + e.getMessage());
              }
          }
      }
  
      @UnsupportedAppUsage
      /* package */ void
      merge(ImsPhoneCall that, State state) {
          // This call is the conference host and the "that" call is the one being merged in.
          // Set the connect time for the conference; this will have been determined when the
          // conference was initially created.
          ImsPhoneConnection imsPhoneConnection = getFirstConnection();
          if (imsPhoneConnection != null) {
              long conferenceConnectTime = imsPhoneConnection.getConferenceConnectTime();
              if (conferenceConnectTime > 0) {
                  imsPhoneConnection.setConnectTime(conferenceConnectTime);
                  imsPhoneConnection.setConnectTimeReal(imsPhoneConnection.getConnectTimeReal());
              } else {
                  if (DBG) {
                      Rlog.d(LOG_TAG, "merge: conference connect time is 0");
                  }
              }
          }
          if (DBG) {
              Rlog.d(LOG_TAG, "merge(" + mCallContext + "): " + that + "state = "
                      + state);
          }
      }
  
      /**
       * Retrieves the {@link ImsCall} for the current {@link ImsPhoneCall}.
       * <p>
       * Marked as {@code VisibleForTesting} so that the
       * {@link com.android.internal.telephony.TelephonyTester} class can inject a test conference
       * event package into a regular ongoing IMS call.
       *
       * @return The {@link ImsCall}.
       */
      @VisibleForTesting
      public ImsCall
      getImsCall() {
          return (getFirstConnection() == null) ? null : getFirstConnection().getImsCall();
      }
  
      /*package*/ static boolean isLocalTone(ImsCall imsCall) {
          if ((imsCall == null) || (imsCall.getCallProfile() == null)
                  || (imsCall.getCallProfile().mMediaProfile == null)) {
              return false;
          }
  
          ImsStreamMediaProfile mediaProfile = imsCall.getCallProfile().mMediaProfile;
  
          return (mediaProfile.mAudioDirection == ImsStreamMediaProfile.DIRECTION_INACTIVE)
                  ? true : false;
      }
  
      public boolean update (ImsPhoneConnection conn, ImsCall imsCall, State state) {
          boolean changed = false;
          State oldState = mState;
  
          //ImsCall.Listener.onCallProgressing can be invoked several times
          //and ringback tone mode can be changed during the call setup procedure
          if (state == State.ALERTING) {
              if (mIsRingbackTonePlaying && !isLocalTone(imsCall)) {
                  getPhone().stopRingbackTone();
                  mIsRingbackTonePlaying = false;
              } else if (!mIsRingbackTonePlaying && isLocalTone(imsCall)) {
                  getPhone().startRingbackTone();
                  mIsRingbackTonePlaying = true;
              }
          } else {
              if (mIsRingbackTonePlaying) {
                  getPhone().stopRingbackTone();
                  mIsRingbackTonePlaying = false;
              }
          }
  
          if ((state != mState) && (state != State.DISCONNECTED)) {
              mState = state;
              changed = true;
          } else if (state == State.DISCONNECTED) {
              changed = true;
          }
  
          if (VDBG) {
              Rlog.v(LOG_TAG, "update : " + mCallContext + " state: " + oldState + " --> " + mState);
          }
  
          return changed;
      }
  
      /* package */ ImsPhoneConnection
      getHandoverConnection() {
          return (ImsPhoneConnection) getEarliestConnection();
      }
  
      public void switchWith(ImsPhoneCall that) {
          if (VDBG) {
              Rlog.v(LOG_TAG, "switchWith : switchCall = " + this + " withCall = " + that);
          }
          synchronized (ImsPhoneCall.class) {
              ImsPhoneCall tmp = new ImsPhoneCall();
              tmp.takeOver(this);
              this.takeOver(that);
              that.takeOver(tmp);
          }
          mOwner.logState();
      }
  
      /**
       * Stops ringback tone playing if it is playing.
       */
      public void maybeStopRingback() {
          if (mIsRingbackTonePlaying) {
              getPhone().stopRingbackTone();
              mIsRingbackTonePlaying = false;
          }
      }
  
      private void takeOver(ImsPhoneCall that) {
          mConnections = that.mConnections;
          mState = that.mState;
          for (Connection c : mConnections) {
              ((ImsPhoneConnection) c).changeParent(this);
          }
      }
  }
  