1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.telecom.Logging; 18 19 import android.annotation.NonNull; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.telecom.Log; 23 import android.text.TextUtils; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 27 import java.util.ArrayList; 28 29 /** 30 * Stores information about a thread's point of entry into that should persist until that thread 31 * exits. 32 * @hide 33 */ 34 public class Session { 35 36 public static final String START_SESSION = "START_SESSION"; 37 public static final String START_EXTERNAL_SESSION = "START_EXTERNAL_SESSION"; 38 public static final String CREATE_SUBSESSION = "CREATE_SUBSESSION"; 39 public static final String CONTINUE_SUBSESSION = "CONTINUE_SUBSESSION"; 40 public static final String END_SUBSESSION = "END_SUBSESSION"; 41 public static final String END_SESSION = "END_SESSION"; 42 43 public static final String SUBSESSION_SEPARATION_CHAR = "->"; 44 public static final String SESSION_SEPARATION_CHAR_CHILD = "_"; 45 public static final String EXTERNAL_INDICATOR = "E-"; 46 public static final String TRUNCATE_STRING = "..."; 47 48 /** 49 * Initial value of mExecutionEndTimeMs and the final value of {@link #getLocalExecutionTime()} 50 * if the Session is canceled. 51 */ 52 public static final int UNDEFINED = -1; 53 54 public static class Info implements Parcelable { 55 public final String sessionId; 56 public final String methodPath; 57 Info(String id, String path)58 private Info(String id, String path) { 59 sessionId = id; 60 methodPath = path; 61 } 62 getInfo(Session s)63 public static Info getInfo (Session s) { 64 // Create Info based on the truncated method path if the session is external, so we do 65 // not get multiple stacking external sessions (unless we have DEBUG level logging or 66 // lower). 67 return new Info(s.getFullSessionId(), s.getFullMethodPath( 68 !Log.DEBUG && s.isSessionExternal())); 69 } 70 71 /** Responsible for creating Info objects for deserialized Parcels. */ 72 public static final Parcelable.Creator<Info> CREATOR = 73 new Parcelable.Creator<Info> () { 74 @Override 75 public Info createFromParcel(Parcel source) { 76 String id = source.readString(); 77 String methodName = source.readString(); 78 return new Info(id, methodName); 79 } 80 81 @Override 82 public Info[] newArray(int size) { 83 return new Info[size]; 84 } 85 }; 86 87 /** {@inheritDoc} */ 88 @Override describeContents()89 public int describeContents() { 90 return 0; 91 } 92 93 /** Writes Info object into a Parcel. */ 94 @Override writeToParcel(Parcel destination, int flags)95 public void writeToParcel(Parcel destination, int flags) { 96 destination.writeString(sessionId); 97 destination.writeString(methodPath); 98 } 99 } 100 101 private String mSessionId; 102 private String mShortMethodName; 103 private long mExecutionStartTimeMs; 104 private long mExecutionEndTimeMs = UNDEFINED; 105 private Session mParentSession; 106 private ArrayList<Session> mChildSessions; 107 private boolean mIsCompleted = false; 108 private boolean mIsExternal = false; 109 private int mChildCounter = 0; 110 // True if this is a subsession that has been started from the same thread as the parent 111 // session. This can happen if Log.startSession(...) is called multiple times on the same 112 // thread in the case of one Telecom entry point method calling another entry point method. 113 // In this case, we can just make this subsession "invisible," but still keep track of it so 114 // that the Log.endSession() calls match up. 115 private boolean mIsStartedFromActiveSession = false; 116 // Optionally provided info about the method/class/component that started the session in order 117 // to make Logging easier. This info will be provided in parentheses along with the session. 118 private String mOwnerInfo; 119 // Cache Full Method path so that recursive population of the full method path only needs to 120 // be calculated once. 121 private String mFullMethodPathCache; 122 Session(String sessionId, String shortMethodName, long startTimeMs, boolean isStartedFromActiveSession, String ownerInfo)123 public Session(String sessionId, String shortMethodName, long startTimeMs, 124 boolean isStartedFromActiveSession, String ownerInfo) { 125 setSessionId(sessionId); 126 setShortMethodName(shortMethodName); 127 mExecutionStartTimeMs = startTimeMs; 128 mParentSession = null; 129 mChildSessions = new ArrayList<>(5); 130 mIsStartedFromActiveSession = isStartedFromActiveSession; 131 mOwnerInfo = ownerInfo; 132 } 133 setSessionId(@onNull String sessionId)134 public void setSessionId(@NonNull String sessionId) { 135 if (sessionId == null) { 136 mSessionId = "?"; 137 } 138 mSessionId = sessionId; 139 } 140 getShortMethodName()141 public String getShortMethodName() { 142 return mShortMethodName; 143 } 144 setShortMethodName(String shortMethodName)145 public void setShortMethodName(String shortMethodName) { 146 if (shortMethodName == null) { 147 shortMethodName = ""; 148 } 149 mShortMethodName = shortMethodName; 150 } 151 setIsExternal(boolean isExternal)152 public void setIsExternal(boolean isExternal) { 153 mIsExternal = isExternal; 154 } 155 isExternal()156 public boolean isExternal() { 157 return mIsExternal; 158 } 159 setParentSession(Session parentSession)160 public void setParentSession(Session parentSession) { 161 mParentSession = parentSession; 162 } 163 addChild(Session childSession)164 public void addChild(Session childSession) { 165 if (childSession != null) { 166 mChildSessions.add(childSession); 167 } 168 } 169 removeChild(Session child)170 public void removeChild(Session child) { 171 if (child != null) { 172 mChildSessions.remove(child); 173 } 174 } 175 getExecutionStartTimeMilliseconds()176 public long getExecutionStartTimeMilliseconds() { 177 return mExecutionStartTimeMs; 178 } 179 setExecutionStartTimeMs(long startTimeMs)180 public void setExecutionStartTimeMs(long startTimeMs) { 181 mExecutionStartTimeMs = startTimeMs; 182 } 183 getParentSession()184 public Session getParentSession() { 185 return mParentSession; 186 } 187 getChildSessions()188 public ArrayList<Session> getChildSessions() { 189 return mChildSessions; 190 } 191 isSessionCompleted()192 public boolean isSessionCompleted() { 193 return mIsCompleted; 194 } 195 isStartedFromActiveSession()196 public boolean isStartedFromActiveSession() { 197 return mIsStartedFromActiveSession; 198 } 199 getInfo()200 public Info getInfo() { 201 return Info.getInfo(this); 202 } 203 204 @VisibleForTesting getSessionId()205 public String getSessionId() { 206 return mSessionId; 207 } 208 209 // Mark this session complete. This will be deleted by Log when all subsessions are complete 210 // as well. markSessionCompleted(long executionEndTimeMs)211 public void markSessionCompleted(long executionEndTimeMs) { 212 mExecutionEndTimeMs = executionEndTimeMs; 213 mIsCompleted = true; 214 } 215 getLocalExecutionTime()216 public long getLocalExecutionTime() { 217 if (mExecutionEndTimeMs == UNDEFINED) { 218 return UNDEFINED; 219 } 220 return mExecutionEndTimeMs - mExecutionStartTimeMs; 221 } 222 getNextChildId()223 public synchronized String getNextChildId() { 224 return String.valueOf(mChildCounter++); 225 } 226 227 // Builds full session id recursively getFullSessionId()228 private String getFullSessionId() { 229 // Cache mParentSession locally to prevent a concurrency problem where 230 // Log.endParentSessions() is called while a logging statement is running (Log.i, for 231 // example) and setting mParentSession to null in a different thread after the null check 232 // occurred. 233 Session parentSession = mParentSession; 234 if (parentSession == null) { 235 return mSessionId; 236 } else { 237 if (Log.VERBOSE) { 238 return parentSession.getFullSessionId() + 239 // Append "_X" to subsession to show subsession designation. 240 SESSION_SEPARATION_CHAR_CHILD + mSessionId; 241 } else { 242 // Only worry about the base ID at the top of the tree. 243 return parentSession.getFullSessionId(); 244 } 245 246 } 247 } 248 249 // Print out the full Session tree from any subsession node printFullSessionTree()250 public String printFullSessionTree() { 251 // Get to the top of the tree 252 Session topNode = this; 253 while (topNode.getParentSession() != null) { 254 topNode = topNode.getParentSession(); 255 } 256 return topNode.printSessionTree(); 257 } 258 259 // Recursively move down session tree using DFS, but print out each node when it is reached. printSessionTree()260 public String printSessionTree() { 261 StringBuilder sb = new StringBuilder(); 262 printSessionTree(0, sb); 263 return sb.toString(); 264 } 265 printSessionTree(int tabI, StringBuilder sb)266 private void printSessionTree(int tabI, StringBuilder sb) { 267 sb.append(toString()); 268 for (Session child : mChildSessions) { 269 sb.append("\n"); 270 for (int i = 0; i <= tabI; i++) { 271 sb.append("\t"); 272 } 273 child.printSessionTree(tabI + 1, sb); 274 } 275 } 276 277 // Recursively concatenate mShortMethodName with the parent Sessions to create full method 278 // path. if truncatePath is set to true, all other external sessions (except for the most 279 // recent) will be truncated to "..." getFullMethodPath(boolean truncatePath)280 public String getFullMethodPath(boolean truncatePath) { 281 StringBuilder sb = new StringBuilder(); 282 getFullMethodPath(sb, truncatePath); 283 return sb.toString(); 284 } 285 getFullMethodPath(StringBuilder sb, boolean truncatePath)286 private synchronized void getFullMethodPath(StringBuilder sb, boolean truncatePath) { 287 // Return cached value for method path. When returning the truncated path, recalculate the 288 // full path without using the cached value. 289 if (!TextUtils.isEmpty(mFullMethodPathCache) && !truncatePath) { 290 sb.append(mFullMethodPathCache); 291 return; 292 } 293 Session parentSession = getParentSession(); 294 boolean isSessionStarted = false; 295 if (parentSession != null) { 296 // Check to see if the session has been renamed yet. If it has not, then the session 297 // has not been continued. 298 isSessionStarted = !mShortMethodName.equals(parentSession.mShortMethodName); 299 parentSession.getFullMethodPath(sb, truncatePath); 300 sb.append(SUBSESSION_SEPARATION_CHAR); 301 } 302 // Encapsulate the external session's method name so it is obvious what part of the session 303 // is external or truncate it if we do not want the entire history. 304 if (isExternal()) { 305 if (truncatePath) { 306 sb.append(TRUNCATE_STRING); 307 } else { 308 sb.append("("); 309 sb.append(mShortMethodName); 310 sb.append(")"); 311 } 312 } else { 313 sb.append(mShortMethodName); 314 } 315 // If we are returning the truncated path, do not save that path as the full path. 316 if (isSessionStarted && !truncatePath) { 317 // Cache this value so that we do not have to do this work next time! 318 // We do not cache the value if the session being evaluated hasn't been continued yet. 319 mFullMethodPathCache = sb.toString(); 320 } 321 } 322 // Recursively move to the top of the tree to see if the parent session is external. isSessionExternal()323 private boolean isSessionExternal() { 324 if (getParentSession() == null) { 325 return isExternal(); 326 } else { 327 return getParentSession().isSessionExternal(); 328 } 329 } 330 331 @Override hashCode()332 public int hashCode() { 333 int result = mSessionId != null ? mSessionId.hashCode() : 0; 334 result = 31 * result + (mShortMethodName != null ? mShortMethodName.hashCode() : 0); 335 result = 31 * result + (int) (mExecutionStartTimeMs ^ (mExecutionStartTimeMs >>> 32)); 336 result = 31 * result + (int) (mExecutionEndTimeMs ^ (mExecutionEndTimeMs >>> 32)); 337 result = 31 * result + (mParentSession != null ? mParentSession.hashCode() : 0); 338 result = 31 * result + (mChildSessions != null ? mChildSessions.hashCode() : 0); 339 result = 31 * result + (mIsCompleted ? 1 : 0); 340 result = 31 * result + mChildCounter; 341 result = 31 * result + (mIsStartedFromActiveSession ? 1 : 0); 342 result = 31 * result + (mOwnerInfo != null ? mOwnerInfo.hashCode() : 0); 343 return result; 344 } 345 346 @Override equals(Object o)347 public boolean equals(Object o) { 348 if (this == o) return true; 349 if (o == null || getClass() != o.getClass()) return false; 350 351 Session session = (Session) o; 352 353 if (mExecutionStartTimeMs != session.mExecutionStartTimeMs) return false; 354 if (mExecutionEndTimeMs != session.mExecutionEndTimeMs) return false; 355 if (mIsCompleted != session.mIsCompleted) return false; 356 if (mChildCounter != session.mChildCounter) return false; 357 if (mIsStartedFromActiveSession != session.mIsStartedFromActiveSession) return false; 358 if (mSessionId != null ? 359 !mSessionId.equals(session.mSessionId) : session.mSessionId != null) 360 return false; 361 if (mShortMethodName != null ? !mShortMethodName.equals(session.mShortMethodName) 362 : session.mShortMethodName != null) 363 return false; 364 if (mParentSession != null ? !mParentSession.equals(session.mParentSession) 365 : session.mParentSession != null) 366 return false; 367 if (mChildSessions != null ? !mChildSessions.equals(session.mChildSessions) 368 : session.mChildSessions != null) 369 return false; 370 return mOwnerInfo != null ? mOwnerInfo.equals(session.mOwnerInfo) 371 : session.mOwnerInfo == null; 372 373 } 374 375 @Override toString()376 public String toString() { 377 if (mParentSession != null && mIsStartedFromActiveSession) { 378 // Log.startSession was called from within another active session. Use the parent's 379 // Id instead of the child to reduce confusion. 380 return mParentSession.toString(); 381 } else { 382 StringBuilder methodName = new StringBuilder(); 383 methodName.append(getFullMethodPath(false /*truncatePath*/)); 384 if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) { 385 methodName.append("(InCall package: "); 386 methodName.append(mOwnerInfo); 387 methodName.append(")"); 388 } 389 return methodName.toString() + "@" + getFullSessionId(); 390 } 391 } 392 } 393