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
17package android.telecom.Logging;
18
19import android.annotation.NonNull;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.telecom.Log;
23import android.text.TextUtils;
24
25import com.android.internal.annotations.VisibleForTesting;
26
27import 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 */
34public 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
58        private Info(String id, String path) {
59            sessionId = id;
60            methodPath = path;
61        }
62
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
89        public int describeContents() {
90            return 0;
91        }
92
93        /** Writes Info object into a Parcel. */
94        @Override
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
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
134    public void setSessionId(@NonNull String sessionId) {
135        if (sessionId == null) {
136            mSessionId = "?";
137        }
138        mSessionId = sessionId;
139    }
140
141    public String getShortMethodName() {
142        return mShortMethodName;
143    }
144
145    public void setShortMethodName(String shortMethodName) {
146        if (shortMethodName == null) {
147            shortMethodName = "";
148        }
149        mShortMethodName = shortMethodName;
150    }
151
152    public void setIsExternal(boolean isExternal) {
153        mIsExternal = isExternal;
154    }
155
156    public boolean isExternal() {
157        return mIsExternal;
158    }
159
160    public void setParentSession(Session parentSession) {
161        mParentSession = parentSession;
162    }
163
164    public void addChild(Session childSession) {
165        if (childSession != null) {
166            mChildSessions.add(childSession);
167        }
168    }
169
170    public void removeChild(Session child) {
171        if (child != null) {
172            mChildSessions.remove(child);
173        }
174    }
175
176    public long getExecutionStartTimeMilliseconds() {
177        return mExecutionStartTimeMs;
178    }
179
180    public void setExecutionStartTimeMs(long startTimeMs) {
181        mExecutionStartTimeMs = startTimeMs;
182    }
183
184    public Session getParentSession() {
185        return mParentSession;
186    }
187
188    public ArrayList<Session> getChildSessions() {
189        return mChildSessions;
190    }
191
192    public boolean isSessionCompleted() {
193        return mIsCompleted;
194    }
195
196    public boolean isStartedFromActiveSession() {
197        return mIsStartedFromActiveSession;
198    }
199
200    public Info getInfo() {
201        return Info.getInfo(this);
202    }
203
204    @VisibleForTesting
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.
211    public void markSessionCompleted(long executionEndTimeMs) {
212        mExecutionEndTimeMs = executionEndTimeMs;
213        mIsCompleted = true;
214    }
215
216    public long getLocalExecutionTime() {
217        if (mExecutionEndTimeMs == UNDEFINED) {
218            return UNDEFINED;
219        }
220        return mExecutionEndTimeMs - mExecutionStartTimeMs;
221    }
222
223    public synchronized String getNextChildId() {
224        return String.valueOf(mChildCounter++);
225    }
226
227    // Builds full session id recursively
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
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.
260    public String printSessionTree() {
261        StringBuilder sb = new StringBuilder();
262        printSessionTree(0, sb);
263        return sb.toString();
264    }
265
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 "..."
280    public String getFullMethodPath(boolean truncatePath) {
281        StringBuilder sb = new StringBuilder();
282        getFullMethodPath(sb, truncatePath);
283        return sb.toString();
284    }
285
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.
323    private boolean isSessionExternal() {
324        if (getParentSession() == null) {
325            return isExternal();
326        } else {
327            return getParentSession().isSessionExternal();
328        }
329    }
330
331    @Override
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
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
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