| /* | |
| * 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.annotation.Nullable; | |
| 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; | |
| public final String ownerInfo; | |
| private Info(String id, String path, String owner) { | |
| sessionId = id; | |
| methodPath = path; | |
| ownerInfo = owner; | |
| } | |
| 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()), s.getOwnerInfo()); | |
| } | |
| public static Info getExternalInfo(Session s, @Nullable String ownerInfo) { | |
| // When creating session information for an existing session, the caller may pass in a | |
| // context to be passed along to the recipient of the external session info. | |
| // So, for example, if telecom has an active session with owner 'cad', and Telecom is | |
| // calling into Telephony and providing external session info, it would pass in 'cast' | |
| // as the owner info. This would result in Telephony seeing owner info 'cad/cast', | |
| // which would make it very clear in the Telephony logs the chain of package calls which | |
| // ultimately resulted in the logs. | |
| String newInfo = ownerInfo != null && s.getOwnerInfo() != null | |
| // If we've got both, concatenate them. | |
| ? s.getOwnerInfo() + "/" + ownerInfo | |
| // Otherwise use whichever is present. | |
| : ownerInfo != null ? ownerInfo : s.getOwnerInfo(); | |
| // 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()), newInfo); | |
| } | |
| /** 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(); | |
| String ownerInfo = source.readString(); | |
| return new Info(id, methodName, ownerInfo); | |
| } | |
| @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); | |
| destination.writeString(ownerInfo); | |
| } | |
| } | |
| 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); | |
| } | |
| public Info getExternalInfo(@Nullable String ownerInfo) { | |
| return Info.getExternalInfo(this, ownerInfo); | |
| } | |
| public String getOwnerInfo() { | |
| return mOwnerInfo; | |
| } | |
| @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) { | |
| // Don't use Telecom's Log.w here or it will cause infinite recursion because it will | |
| // try to add session information to this logging statement, which will cause it to hit | |
| // this condition again and so on... | |
| android.util.Slog.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) { | |
| // Don't use Telecom's Log.w here or it will cause infinite recursion because it | |
| // will try to add session information to this logging statement, which will cause | |
| // it to hit this condition again and so on... | |
| android.util.Slog.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) { | |
| // Don't use Telecom's Log.w here or it will cause infinite recursion because it will | |
| // try to add session information to this logging statement, which will cause it to hit | |
| // this condition again and so on... | |
| android.util.Slog.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) { | |
| // Don't use Telecom's Log.w here or it will cause infinite recursion because it will | |
| // try to add session information to this logging statement, which will cause it to hit | |
| // this condition again and so on... | |
| android.util.Slog.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() { | |
| Session sessionToPrint = this; | |
| if (getParentSession() != null && isStartedFromActiveSession()) { | |
| // Log.startSession was called from within another active session. Use the parent's | |
| // Id instead of the child to reduce confusion. | |
| sessionToPrint = getRootSession("toString"); | |
| } | |
| StringBuilder methodName = new StringBuilder(); | |
| methodName.append(sessionToPrint.getFullMethodPath(false /*truncatePath*/)); | |
| if (sessionToPrint.getOwnerInfo() != null && !sessionToPrint.getOwnerInfo().isEmpty()) { | |
| methodName.append("("); | |
| methodName.append(sessionToPrint.getOwnerInfo()); | |
| methodName.append(")"); | |
| } | |
| return methodName.toString() + "@" + sessionToPrint.getFullSessionId(); | |
| } | |
| } |