content: /*
   * Copyright (C) 2016 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 android.telecom.Logging;
  
  import android.annotation.NonNull;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.telecom.Log;
  import android.text.TextUtils;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.util.ArrayList;
  
  /**
   * Stores information about a thread's point of entry into that should persist until that thread
   * exits.
   * @hide
   */
  public class Session {
  
      public static final String LOG_TAG = "Session";
  
      public static final String START_SESSION = "START_SESSION";
      public static final String START_EXTERNAL_SESSION = "START_EXTERNAL_SESSION";
      public static final String CREATE_SUBSESSION = "CREATE_SUBSESSION";
      public static final String CONTINUE_SUBSESSION = "CONTINUE_SUBSESSION";
      public static final String END_SUBSESSION = "END_SUBSESSION";
      public static final String END_SESSION = "END_SESSION";
  
      public static final String SUBSESSION_SEPARATION_CHAR = "->";
      public static final String SESSION_SEPARATION_CHAR_CHILD = "_";
      public static final String EXTERNAL_INDICATOR = "E-";
      public static final String TRUNCATE_STRING = "...";
  
      // Prevent infinite recursion by setting a reasonable limit.
      private static final int SESSION_RECURSION_LIMIT = 25;
  
      /**
       * Initial value of mExecutionEndTimeMs and the final value of {@link #getLocalExecutionTime()}
       * if the Session is canceled.
       */
      public static final int UNDEFINED = -1;
  
      public static class Info implements Parcelable {
          public final String sessionId;
          public final String methodPath;
  
          private Info(String id, String path) {
              sessionId = id;
              methodPath = path;
          }
  
          public static Info getInfo (Session s) {
              // Create Info based on the truncated method path if the session is external, so we do
              // not get multiple stacking external sessions (unless we have DEBUG level logging or
              // lower).
              return new Info(s.getFullSessionId(), s.getFullMethodPath(
                      !Log.DEBUG && s.isSessionExternal()));
          }
  
          /** Responsible for creating Info objects for deserialized Parcels. */
          public static final @android.annotation.NonNull Parcelable.Creator<Info> CREATOR =
                  new Parcelable.Creator<Info> () {
                      @Override
                      public Info createFromParcel(Parcel source) {
                          String id = source.readString();
                          String methodName = source.readString();
                          return new Info(id, methodName);
                      }
  
                      @Override
                      public Info[] newArray(int size) {
                          return new Info[size];
                      }
                  };
  
          /** {@inheritDoc} */
          @Override
          public int describeContents() {
              return 0;
          }
  
          /** Writes Info object into a Parcel. */
          @Override
          public void writeToParcel(Parcel destination, int flags) {
              destination.writeString(sessionId);
              destination.writeString(methodPath);
          }
      }
  
      private String mSessionId;
      private String mShortMethodName;
      private long mExecutionStartTimeMs;
      private long mExecutionEndTimeMs = UNDEFINED;
      private Session mParentSession;
      private ArrayList<Session> mChildSessions;
      private boolean mIsCompleted = false;
      private boolean mIsExternal = false;
      private int mChildCounter = 0;
      // True if this is a subsession that has been started from the same thread as the parent
      // session. This can happen if Log.startSession(...) is called multiple times on the same
      // thread in the case of one Telecom entry point method calling another entry point method.
      // In this case, we can just make this subsession "invisible," but still keep track of it so
      // that the Log.endSession() calls match up.
      private boolean mIsStartedFromActiveSession = false;
      // Optionally provided info about the method/class/component that started the session in order
      // to make Logging easier. This info will be provided in parentheses along with the session.
      private String mOwnerInfo;
      // Cache Full Method path so that recursive population of the full method path only needs to
      // be calculated once.
      private String mFullMethodPathCache;
  
      public Session(String sessionId, String shortMethodName, long startTimeMs,
              boolean isStartedFromActiveSession, String ownerInfo) {
          setSessionId(sessionId);
          setShortMethodName(shortMethodName);
          mExecutionStartTimeMs = startTimeMs;
          mParentSession = null;
          mChildSessions = new ArrayList<>(5);
          mIsStartedFromActiveSession = isStartedFromActiveSession;
          mOwnerInfo = ownerInfo;
      }
  
      public void setSessionId(@NonNull String sessionId) {
          if (sessionId == null) {
              mSessionId = "?";
          }
          mSessionId = sessionId;
      }
  
      public String getShortMethodName() {
          return mShortMethodName;
      }
  
      public void setShortMethodName(String shortMethodName) {
          if (shortMethodName == null) {
              shortMethodName = "";
          }
          mShortMethodName = shortMethodName;
      }
  
      public void setIsExternal(boolean isExternal) {
          mIsExternal = isExternal;
      }
  
      public boolean isExternal() {
          return mIsExternal;
      }
  
      public void setParentSession(Session parentSession) {
          mParentSession = parentSession;
      }
  
      public void addChild(Session childSession) {
          if (childSession != null) {
              mChildSessions.add(childSession);
          }
      }
  
      public void removeChild(Session child) {
          if (child != null) {
              mChildSessions.remove(child);
          }
      }
  
      public long getExecutionStartTimeMilliseconds() {
          return mExecutionStartTimeMs;
      }
  
      public void setExecutionStartTimeMs(long startTimeMs) {
          mExecutionStartTimeMs = startTimeMs;
      }
  
      public Session getParentSession() {
          return mParentSession;
      }
  
      public ArrayList<Session> getChildSessions() {
          return mChildSessions;
      }
  
      public boolean isSessionCompleted() {
          return mIsCompleted;
      }
  
      public boolean isStartedFromActiveSession() {
          return mIsStartedFromActiveSession;
      }
  
      public Info getInfo() {
          return Info.getInfo(this);
      }
  
      @VisibleForTesting
      public String getSessionId() {
          return mSessionId;
      }
  
      // Mark this session complete. This will be deleted by Log when all subsessions are complete
      // as well.
      public void markSessionCompleted(long executionEndTimeMs) {
          mExecutionEndTimeMs = executionEndTimeMs;
          mIsCompleted = true;
      }
  
      public long getLocalExecutionTime() {
          if (mExecutionEndTimeMs == UNDEFINED) {
              return UNDEFINED;
          }
          return mExecutionEndTimeMs - mExecutionStartTimeMs;
      }
  
      public synchronized String getNextChildId() {
          return String.valueOf(mChildCounter++);
      }
  
      // Builds full session id recursively
      private String getFullSessionId() {
          return getFullSessionId(0);
      }
  
      // keep track of calls and bail if we hit the recursion limit
      private String getFullSessionId(int parentCount) {
          if (parentCount >= SESSION_RECURSION_LIMIT) {
              Log.w(LOG_TAG, "getFullSessionId: Hit recursion limit!");
              return TRUNCATE_STRING + mSessionId;
          }
          // Cache mParentSession locally to prevent a concurrency problem where
          // Log.endParentSessions() is called while a logging statement is running (Log.i, for
          // example) and setting mParentSession to null in a different thread after the null check
          // occurred.
          Session parentSession = mParentSession;
          if (parentSession == null) {
              return mSessionId;
          } else {
              if (Log.VERBOSE) {
                  return parentSession.getFullSessionId(parentCount + 1)
                          // Append "_X" to subsession to show subsession designation.
                          + SESSION_SEPARATION_CHAR_CHILD + mSessionId;
              } else {
                  // Only worry about the base ID at the top of the tree.
                  return parentSession.getFullSessionId(parentCount + 1);
              }
  
          }
      }
  
      private Session getRootSession(String callingMethod) {
          int currParentCount = 0;
          Session topNode = this;
          while (topNode.getParentSession() != null) {
              if (currParentCount >= SESSION_RECURSION_LIMIT) {
                  Log.w(LOG_TAG, "getRootSession: Hit recursion limit from " + callingMethod);
                  break;
              }
              topNode = topNode.getParentSession();
              currParentCount++;
          }
          return topNode;
      }
  
      // Print out the full Session tree from any subsession node
      public String printFullSessionTree() {
          return getRootSession("printFullSessionTree").printSessionTree();
      }
  
      // Recursively move down session tree using DFS, but print out each node when it is reached.
      private String printSessionTree() {
          StringBuilder sb = new StringBuilder();
          printSessionTree(0, sb, 0);
          return sb.toString();
      }
  
      private void printSessionTree(int tabI, StringBuilder sb, int currChildCount) {
          // Prevent infinite recursion.
          if (currChildCount >= SESSION_RECURSION_LIMIT) {
              Log.w(LOG_TAG, "printSessionTree: Hit recursion limit!");
              sb.append(TRUNCATE_STRING);
              return;
          }
          sb.append(toString());
          for (Session child : mChildSessions) {
              sb.append("\n");
              for (int i = 0; i <= tabI; i++) {
                  sb.append("\t");
              }
              child.printSessionTree(tabI + 1, sb, currChildCount + 1);
          }
      }
  
      // Recursively concatenate mShortMethodName with the parent Sessions to create full method
      // path. if truncatePath is set to true, all other external sessions (except for the most
      // recent) will be truncated to "..."
      public String getFullMethodPath(boolean truncatePath) {
          StringBuilder sb = new StringBuilder();
          getFullMethodPath(sb, truncatePath, 0);
          return sb.toString();
      }
  
      private synchronized void getFullMethodPath(StringBuilder sb, boolean truncatePath,
              int parentCount) {
          if (parentCount >= SESSION_RECURSION_LIMIT) {
              Log.w(LOG_TAG, "getFullMethodPath: Hit recursion limit!");
              sb.append(TRUNCATE_STRING);
              return;
          }
          // Return cached value for method path. When returning the truncated path, recalculate the
          // full path without using the cached value.
          if (!TextUtils.isEmpty(mFullMethodPathCache) && !truncatePath) {
              sb.append(mFullMethodPathCache);
              return;
          }
          Session parentSession = getParentSession();
          boolean isSessionStarted = false;
          if (parentSession != null) {
              // Check to see if the session has been renamed yet. If it has not, then the session
              // has not been continued.
              isSessionStarted = !mShortMethodName.equals(parentSession.mShortMethodName);
              parentSession.getFullMethodPath(sb, truncatePath, parentCount + 1);
              sb.append(SUBSESSION_SEPARATION_CHAR);
          }
          // Encapsulate the external session's method name so it is obvious what part of the session
          // is external or truncate it if we do not want the entire history.
          if (isExternal()) {
              if (truncatePath) {
                  sb.append(TRUNCATE_STRING);
              } else {
                  sb.append("(");
                  sb.append(mShortMethodName);
                  sb.append(")");
              }
          } else {
              sb.append(mShortMethodName);
          }
          // If we are returning the truncated path, do not save that path as the full path.
          if (isSessionStarted && !truncatePath) {
              // Cache this value so that we do not have to do this work next time!
              // We do not cache the value if the session being evaluated hasn't been continued yet.
              mFullMethodPathCache = sb.toString();
          }
      }
  
      // Recursively move to the top of the tree to see if the parent session is external.
      private boolean isSessionExternal() {
          return getRootSession("isSessionExternal").isExternal();
      }
  
      @Override
      public int hashCode() {
          int result = mSessionId != null ? mSessionId.hashCode() : 0;
          result = 31 * result + (mShortMethodName != null ? mShortMethodName.hashCode() : 0);
          result = 31 * result + (int) (mExecutionStartTimeMs ^ (mExecutionStartTimeMs >>> 32));
          result = 31 * result + (int) (mExecutionEndTimeMs ^ (mExecutionEndTimeMs >>> 32));
          result = 31 * result + (mParentSession != null ? mParentSession.hashCode() : 0);
          result = 31 * result + (mChildSessions != null ? mChildSessions.hashCode() : 0);
          result = 31 * result + (mIsCompleted ? 1 : 0);
          result = 31 * result + mChildCounter;
          result = 31 * result + (mIsStartedFromActiveSession ? 1 : 0);
          result = 31 * result + (mOwnerInfo != null ? mOwnerInfo.hashCode() : 0);
          return result;
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
  
          Session session = (Session) o;
  
          if (mExecutionStartTimeMs != session.mExecutionStartTimeMs) return false;
          if (mExecutionEndTimeMs != session.mExecutionEndTimeMs) return false;
          if (mIsCompleted != session.mIsCompleted) return false;
          if (mChildCounter != session.mChildCounter) return false;
          if (mIsStartedFromActiveSession != session.mIsStartedFromActiveSession) return false;
          if (mSessionId != null ?
                  !mSessionId.equals(session.mSessionId) : session.mSessionId != null)
              return false;
          if (mShortMethodName != null ? !mShortMethodName.equals(session.mShortMethodName)
                  : session.mShortMethodName != null)
              return false;
          if (mParentSession != null ? !mParentSession.equals(session.mParentSession)
                  : session.mParentSession != null)
              return false;
          if (mChildSessions != null ? !mChildSessions.equals(session.mChildSessions)
                  : session.mChildSessions != null)
              return false;
          return mOwnerInfo != null ? mOwnerInfo.equals(session.mOwnerInfo)
                  : session.mOwnerInfo == null;
  
      }
  
      @Override
      public String toString() {
          if (mParentSession != null && mIsStartedFromActiveSession) {
              // Log.startSession was called from within another active session. Use the parent's
              // Id instead of the child to reduce confusion.
              return mParentSession.toString();
          } else {
              StringBuilder methodName = new StringBuilder();
              methodName.append(getFullMethodPath(false /*truncatePath*/));
              if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) {
                  methodName.append("(InCall package: ");
                  methodName.append(mOwnerInfo);
                  methodName.append(")");
              }
              return methodName.toString() + "@" + getFullSessionId();
          }
      }
  }
  