1 /* 2 * Copyright (C) 2024 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 package com.android.quickstep.util; 17 18 import androidx.annotation.NonNull; 19 import androidx.annotation.Nullable; 20 21 import java.io.PrintWriter; 22 import java.text.SimpleDateFormat; 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.Collections; 26 import java.util.Date; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Objects; 30 31 /** 32 * A log to keep track of the active gesture. 33 */ 34 public class ActiveGestureLog { 35 36 private static final int MAX_GESTURES_TRACKED = 15; 37 38 public static final ActiveGestureLog INSTANCE = new ActiveGestureLog(); 39 40 private boolean mIsFullyGesturalNavMode; 41 42 /** 43 * NOTE: This value should be kept same as 44 * ActivityTaskManagerService#INTENT_EXTRA_LOG_TRACE_ID in platform 45 */ 46 public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID"; 47 48 private final EventLog[] logs; 49 private int nextIndex; 50 private int mCurrentLogId = 100; 51 ActiveGestureLog()52 private ActiveGestureLog() { 53 this.logs = new EventLog[MAX_GESTURES_TRACKED]; 54 this.nextIndex = 0; 55 } 56 57 /** 58 * Track the given event for error detection. 59 * 60 * @param gestureEvent GestureEvent representing an event during the current gesture's 61 * execution. 62 */ trackEvent(@ullable ActiveGestureErrorDetector.GestureEvent gestureEvent)63 public void trackEvent(@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { 64 addLog(CompoundString.NO_OP, gestureEvent); 65 } 66 67 /** 68 * Adds a log to be printed at log-dump-time. 69 */ addLog(@onNull String event)70 public void addLog(@NonNull String event) { 71 addLog(event, null); 72 } 73 74 /** 75 * Adds a log to be printed at log-dump-time and track the associated event for error detection. 76 * 77 * @param gestureEvent GestureEvent representing the event being logged. 78 */ addLog( @onNull String event, @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent)79 public void addLog( 80 @NonNull String event, @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { 81 addLog(new CompoundString(event), gestureEvent); 82 } 83 addLog(@onNull CompoundString compoundString)84 public void addLog(@NonNull CompoundString compoundString) { 85 addLog(compoundString, null); 86 } 87 addLog( @onNull CompoundString compoundString, @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent)88 public void addLog( 89 @NonNull CompoundString compoundString, 90 @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { 91 EventLog lastEventLog = logs[(nextIndex + logs.length - 1) % logs.length]; 92 if (lastEventLog == null || mCurrentLogId != lastEventLog.logId) { 93 EventLog eventLog = new EventLog(mCurrentLogId, mIsFullyGesturalNavMode); 94 EventEntry eventEntry = new EventEntry(); 95 96 eventEntry.update(compoundString, gestureEvent); 97 eventLog.eventEntries.add(eventEntry); 98 logs[nextIndex] = eventLog; 99 nextIndex = (nextIndex + 1) % logs.length; 100 return; 101 } 102 103 // Update the last EventLog 104 List<EventEntry> lastEventEntries = lastEventLog.eventEntries; 105 EventEntry lastEntry = !lastEventEntries.isEmpty() 106 ? lastEventEntries.get(lastEventEntries.size() - 1) : null; 107 108 // Update the last EventEntry if it's a duplicate 109 if (isEntrySame(lastEntry, compoundString, gestureEvent)) { 110 lastEntry.duplicateCount++; 111 return; 112 } 113 EventEntry eventEntry = new EventEntry(); 114 115 eventEntry.update(compoundString, gestureEvent); 116 lastEventEntries.add(eventEntry); 117 } 118 dump(String prefix, PrintWriter writer)119 public void dump(String prefix, PrintWriter writer) { 120 writer.println(prefix + "ActiveGestureErrorDetector:"); 121 for (int i = 0; i < logs.length; i++) { 122 EventLog eventLog = logs[(nextIndex + i) % logs.length]; 123 if (eventLog == null) { 124 continue; 125 } 126 ActiveGestureErrorDetector.analyseAndDump(prefix + '\t', writer, eventLog); 127 } 128 129 writer.println(prefix + "ActiveGestureLog history:"); 130 SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSSZ ", Locale.US); 131 Date date = new Date(); 132 for (int i = 0; i < logs.length; i++) { 133 EventLog eventLog = logs[(nextIndex + i) % logs.length]; 134 if (eventLog == null) { 135 continue; 136 } 137 138 writer.println(prefix + "\tLogs for logId: " + eventLog.logId); 139 for (EventEntry eventEntry : eventLog.eventEntries) { 140 if (eventEntry.mCompoundString.mIsNoOp) { 141 continue; 142 } 143 date.setTime(eventEntry.time); 144 145 StringBuilder msg = new StringBuilder(prefix + "\t\t") 146 .append(sdf.format(date)) 147 .append(eventEntry.mCompoundString); 148 if (eventEntry.duplicateCount > 0) { 149 msg.append(" & ").append(eventEntry.duplicateCount).append(" similar events"); 150 } 151 writer.println(msg); 152 } 153 } 154 } 155 156 /** 157 * Increments and returns the current log ID. This should be used every time a new log trace 158 * is started. 159 */ incrementLogId()160 public int incrementLogId() { 161 return mCurrentLogId++; 162 } 163 setIsFullyGesturalNavMode(boolean isFullyGesturalNavMode)164 public void setIsFullyGesturalNavMode(boolean isFullyGesturalNavMode) { 165 mIsFullyGesturalNavMode = isFullyGesturalNavMode; 166 } 167 168 /** Returns the current log ID. This should be used when a log trace is being reused. */ getLogId()169 public int getLogId() { 170 return mCurrentLogId; 171 } 172 isEntrySame( EventEntry entry, CompoundString compoundString, ActiveGestureErrorDetector.GestureEvent gestureEvent)173 private boolean isEntrySame( 174 EventEntry entry, 175 CompoundString compoundString, 176 ActiveGestureErrorDetector.GestureEvent gestureEvent) { 177 return entry != null 178 && entry.mCompoundString.equals(compoundString) 179 && entry.gestureEvent == gestureEvent; 180 } 181 182 /** A single event entry. */ 183 protected static class EventEntry { 184 185 @NonNull private CompoundString mCompoundString; 186 private ActiveGestureErrorDetector.GestureEvent gestureEvent; 187 private long time; 188 private int duplicateCount; 189 EventEntry()190 private EventEntry() {} 191 192 @Nullable getGestureEvent()193 protected ActiveGestureErrorDetector.GestureEvent getGestureEvent() { 194 return gestureEvent; 195 } 196 getDuplicateCount()197 public int getDuplicateCount() { 198 return duplicateCount; 199 } 200 update( @onNull CompoundString compoundString, ActiveGestureErrorDetector.GestureEvent gestureEvent)201 private void update( 202 @NonNull CompoundString compoundString, 203 ActiveGestureErrorDetector.GestureEvent gestureEvent) { 204 this.mCompoundString = compoundString; 205 this.gestureEvent = gestureEvent; 206 time = System.currentTimeMillis(); 207 duplicateCount = 0; 208 } 209 getTime()210 public long getTime() { 211 return time; 212 } 213 } 214 215 /** An entire log of entries associated with a single log ID */ 216 protected static class EventLog { 217 218 protected final List<EventEntry> eventEntries = 219 Collections.synchronizedList(new ArrayList<>()); 220 protected final int logId; 221 protected final boolean mIsFullyGesturalNavMode; 222 EventLog(int logId, boolean isFullyGesturalNavMode)223 private EventLog(int logId, boolean isFullyGesturalNavMode) { 224 this.logId = logId; 225 mIsFullyGesturalNavMode = isFullyGesturalNavMode; 226 } 227 } 228 229 /** A buildable string stored as an array for memory efficiency. */ 230 public static class CompoundString { 231 232 public static final CompoundString NO_OP = new CompoundString(true); 233 234 private final List<String> mSubstrings; 235 private final List<Object> mArgs; 236 237 private final boolean mIsNoOp; 238 newEmptyString()239 public static CompoundString newEmptyString() { 240 return new CompoundString(false); 241 } 242 CompoundString(boolean isNoOp)243 private CompoundString(boolean isNoOp) { 244 mIsNoOp = isNoOp; 245 mSubstrings = mIsNoOp ? null : new ArrayList<>(); 246 mArgs = mIsNoOp ? null : new ArrayList<>(); 247 } 248 CompoundString(String substring, Object... args)249 public CompoundString(String substring, Object... args) { 250 this(substring == null); 251 252 append(substring, args); 253 } 254 append(CompoundString substring)255 public CompoundString append(CompoundString substring) { 256 if (mIsNoOp || substring.mIsNoOp) { 257 return this; 258 } 259 mSubstrings.addAll(substring.mSubstrings); 260 mArgs.addAll(substring.mArgs); 261 262 return this; 263 } 264 append(String substring, Object... args)265 public CompoundString append(String substring, Object... args) { 266 if (mIsNoOp) { 267 return this; 268 } 269 mSubstrings.add(substring); 270 mArgs.addAll(Arrays.stream(args).toList()); 271 272 return this; 273 } 274 275 @Override toString()276 public String toString() { 277 if (mIsNoOp) return null; 278 StringBuilder sb = new StringBuilder(); 279 for (String substring : mSubstrings) { 280 sb.append(substring); 281 } 282 return String.format(sb.toString(), mArgs.toArray()); 283 } 284 285 @Override hashCode()286 public int hashCode() { 287 return Objects.hash(mIsNoOp, mSubstrings, mArgs); 288 } 289 290 @Override equals(Object obj)291 public boolean equals(Object obj) { 292 if (!(obj instanceof CompoundString other)) { 293 return false; 294 } 295 return (mIsNoOp == other.mIsNoOp) 296 && Objects.equals(mSubstrings, other.mSubstrings) 297 && Objects.equals(mArgs, other.mArgs); 298 } 299 } 300 } 301