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 android.util.ArraySet; 19 20 import androidx.annotation.NonNull; 21 22 import java.io.PrintWriter; 23 import java.util.Set; 24 25 /** 26 * Utility class for tracking gesture navigation events as they happen, then detecting and reporting 27 * known issues at log dump time. 28 */ 29 public class ActiveGestureErrorDetector { 30 31 /** 32 * Enums associated to gesture navigation events. 33 */ 34 public enum GestureEvent { 35 MOTION_DOWN, MOTION_UP, MOTION_MOVE, SET_END_TARGET, SET_END_TARGET_HOME, 36 SET_END_TARGET_NEW_TASK, SET_END_TARGET_ALL_APPS, ON_SETTLED_ON_END_TARGET, 37 ON_START_RECENTS_ANIMATION, ON_FINISH_RECENTS_ANIMATION, ON_CANCEL_RECENTS_ANIMATION, 38 START_RECENTS_ANIMATION, FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION, 39 SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT, 40 SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED, 41 FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING, 42 INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING, 43 QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED, 44 45 /** 46 * These GestureEvents are specifically associated to state flags that get set in 47 * {@link com.android.quickstep.MultiStateCallback}. If a state flag needs to be tracked 48 * for error detection, an enum should be added here and that state flag-enum pair should 49 * be added to the state flag's container class' {@code getTrackedEventForState} method. 50 */ 51 STATE_GESTURE_STARTED, STATE_GESTURE_COMPLETED, STATE_GESTURE_CANCELLED, 52 STATE_END_TARGET_ANIMATION_FINISHED, STATE_RECENTS_SCROLLING_FINISHED, 53 STATE_CAPTURE_SCREENSHOT, STATE_SCREENSHOT_CAPTURED, STATE_HANDLER_INVALIDATED, 54 STATE_RECENTS_ANIMATION_CANCELED, STATE_LAUNCHER_DRAWN(true, false); 55 56 public final boolean mLogEvent; 57 public final boolean mTrackEvent; 58 GestureEvent()59 GestureEvent() { 60 this(false, true); 61 } 62 GestureEvent(boolean logEvent, boolean trackEvent)63 GestureEvent(boolean logEvent, boolean trackEvent) { 64 mLogEvent = logEvent; 65 mTrackEvent = trackEvent; 66 } 67 } 68 ActiveGestureErrorDetector()69 private ActiveGestureErrorDetector() {} 70 71 private static final long ON_START_RECENT_ANIMATION_TIME_LIMIT = 500; 72 analyseAndDump( @onNull String prefix, @NonNull PrintWriter writer, @NonNull ActiveGestureLog.EventLog eventLog)73 protected static void analyseAndDump( 74 @NonNull String prefix, 75 @NonNull PrintWriter writer, 76 @NonNull ActiveGestureLog.EventLog eventLog) { 77 writer.println(prefix + "Error messages for gesture ID: " + eventLog.logId); 78 if (!eventLog.mIsFullyGesturalNavMode) { 79 writer.println(prefix 80 + "\tSkipping gesture error detection because gesture navigation not enabled"); 81 return; 82 } 83 84 boolean errorDetected = false; 85 // Use a Set since the order is inherently checked in the loop. 86 final Set<GestureEvent> encounteredEvents = new ArraySet<>(); 87 // Set flags and check order of operations. 88 long lastStartRecentAnimationEventEntryTime = 0; 89 for (ActiveGestureLog.EventEntry eventEntry : eventLog.eventEntries) { 90 GestureEvent gestureEvent = eventEntry.getGestureEvent(); 91 if (gestureEvent == null) { 92 continue; 93 } 94 encounteredEvents.add(gestureEvent); 95 96 switch (gestureEvent) { 97 case MOTION_UP: 98 errorDetected |= printErrorIfTrue( 99 !encounteredEvents.contains(GestureEvent.MOTION_DOWN), 100 prefix, 101 /* errorMessage= */ "Motion up detected before/without" 102 + " motion down.", 103 writer); 104 break; 105 case ON_SETTLED_ON_END_TARGET: 106 errorDetected |= printErrorIfTrue( 107 !encounteredEvents.contains(GestureEvent.SET_END_TARGET), 108 prefix, 109 /* errorMessage= */ "onSettledOnEndTarget called " 110 + "before/without setEndTarget.", 111 writer); 112 break; 113 case FINISH_RECENTS_ANIMATION: 114 errorDetected |= printErrorIfTrue( 115 !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION), 116 prefix, 117 /* errorMessage= */ "finishRecentsAnimation called " 118 + "before/without startRecentsAnimation.", 119 writer); 120 break; 121 case CANCEL_RECENTS_ANIMATION: 122 errorDetected |= printErrorIfTrue( 123 !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION), 124 prefix, 125 /* errorMessage= */ "cancelRecentsAnimation called " 126 + "before/without startRecentsAnimation.", 127 writer); 128 break; 129 case CLEANUP_SCREENSHOT: 130 errorDetected |= printErrorIfTrue( 131 !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED), 132 prefix, 133 /* errorMessage= */ "recents activity screenshot was " 134 + "cleaned up before/without STATE_SCREENSHOT_CAPTURED " 135 + "being set.", 136 writer); 137 break; 138 case SCROLLER_ANIMATION_ABORTED: 139 errorDetected |= printErrorIfTrue( 140 encounteredEvents.contains(GestureEvent.SET_END_TARGET_HOME) 141 && !encounteredEvents.contains( 142 GestureEvent.ON_SETTLED_ON_END_TARGET), 143 prefix, 144 /* errorMessage= */ "recents view scroller animation " 145 + "aborted after setting end target HOME, but before" 146 + " settling on end target.", 147 writer); 148 break; 149 case TASK_APPEARED: 150 errorDetected |= printErrorIfTrue( 151 !encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED), 152 prefix, 153 /* errorMessage= */ "onTasksAppeared was not expected to be called", 154 writer); 155 if (encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED)) { 156 // Remove both events so that we can properly detect following errors. 157 encounteredEvents.remove(GestureEvent.EXPECTING_TASK_APPEARED); 158 encounteredEvents.remove(GestureEvent.TASK_APPEARED); 159 } 160 break; 161 case LAUNCHER_DESTROYED: 162 errorDetected |= printErrorIfTrue( 163 true, 164 prefix, 165 /* errorMessage= */ "Launcher destroyed mid-gesture", 166 writer); 167 break; 168 case STATE_GESTURE_COMPLETED: 169 errorDetected |= printErrorIfTrue( 170 !encounteredEvents.contains(GestureEvent.MOTION_UP), 171 prefix, 172 /* errorMessage= */ "STATE_GESTURE_COMPLETED set " 173 + "before/without motion up.", 174 writer); 175 errorDetected |= printErrorIfTrue( 176 !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED), 177 prefix, 178 /* errorMessage= */ "STATE_GESTURE_COMPLETED set " 179 + "before/without STATE_GESTURE_STARTED.", 180 writer); 181 break; 182 case STATE_GESTURE_CANCELLED: 183 errorDetected |= printErrorIfTrue( 184 !encounteredEvents.contains(GestureEvent.MOTION_UP), 185 prefix, 186 /* errorMessage= */ "STATE_GESTURE_CANCELLED set " 187 + "before/without motion up.", 188 writer); 189 errorDetected |= printErrorIfTrue( 190 !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED), 191 prefix, 192 /* errorMessage= */ "STATE_GESTURE_CANCELLED set " 193 + "before/without STATE_GESTURE_STARTED.", 194 writer); 195 break; 196 case STATE_SCREENSHOT_CAPTURED: 197 errorDetected |= printErrorIfTrue( 198 !encounteredEvents.contains(GestureEvent.STATE_CAPTURE_SCREENSHOT), 199 prefix, 200 /* errorMessage= */ "STATE_SCREENSHOT_CAPTURED set " 201 + "before/without STATE_CAPTURE_SCREENSHOT.", 202 writer); 203 break; 204 case STATE_RECENTS_SCROLLING_FINISHED: 205 errorDetected |= printErrorIfTrue( 206 !encounteredEvents.contains( 207 GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK), 208 prefix, 209 /* errorMessage= */ "STATE_RECENTS_SCROLLING_FINISHED " 210 + "set before/without calling " 211 + "setOnPageTransitionEndCallback.", 212 writer); 213 break; 214 case STATE_RECENTS_ANIMATION_CANCELED: 215 errorDetected |= printErrorIfTrue( 216 !encounteredEvents.contains( 217 GestureEvent.START_RECENTS_ANIMATION), 218 prefix, 219 /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED " 220 + "set before/without startRecentsAnimation.", 221 writer); 222 break; 223 case RECENT_TASKS_MISSING: 224 errorDetected |= printErrorIfTrue( 225 true, 226 prefix, 227 /* errorMessage= */ "SystemUiProxy.mRecentTasks missing," 228 + " couldn't start the recents activity", 229 writer); 230 break; 231 case ON_START_RECENTS_ANIMATION: 232 errorDetected |= printErrorIfTrue( 233 !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION), 234 prefix, 235 /* errorMessage= */ "ON_START_RECENTS_ANIMATION " 236 + "onAnimationStart callback ran before startRecentsAnimation", 237 writer); 238 errorDetected |= printErrorIfTrue( 239 eventEntry.getTime() - lastStartRecentAnimationEventEntryTime 240 > ON_START_RECENT_ANIMATION_TIME_LIMIT, 241 prefix, 242 /* errorMessage= */"ON_START_RECENTS_ANIMATION " 243 + "startRecentsAnimation was never called or onAnimationStart " 244 + "callback was called more than 500 ms after " 245 + "startRecentsAnimation.", 246 writer); 247 lastStartRecentAnimationEventEntryTime = 0; 248 break; 249 case ON_CANCEL_RECENTS_ANIMATION: 250 errorDetected |= printErrorIfTrue( 251 !encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION), 252 prefix, 253 /* errorMessage= */ "ON_CANCEL_RECENTS_ANIMATION " 254 + "onAnimationCanceled callback ran before onAnimationStart " 255 + "callback", 256 writer); 257 break; 258 case ON_FINISH_RECENTS_ANIMATION: 259 errorDetected |= printErrorIfTrue( 260 !encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION), 261 prefix, 262 /* errorMessage= */ "ON_FINISH_RECENTS_ANIMATION " 263 + "onAnimationFinished callback ran before onAnimationStart " 264 + "callback", 265 writer); 266 break; 267 case INVALID_VELOCITY_ON_SWIPE_UP: 268 errorDetected |= printErrorIfTrue( 269 true, 270 prefix, 271 /* errorMessage= */ "invalid velocity on swipe up gesture.", 272 writer); 273 break; 274 case START_RECENTS_ANIMATION: 275 lastStartRecentAnimationEventEntryTime = eventEntry.getTime(); 276 break; 277 case RECENTS_ANIMATION_START_PENDING: 278 errorDetected |= printErrorIfTrue( 279 true, 280 prefix, 281 /* errorMessage= */ (eventEntry.getDuplicateCount() + 1) 282 + " gesture(s) attempted while a requested recents" 283 + " animation is still pending.", 284 writer); 285 break; 286 case QUICK_SWITCH_FROM_HOME_FALLBACK: 287 errorDetected |= printErrorIfTrue( 288 true, 289 prefix, 290 /* errorMessage= */ "Quick switch from home fallback case: the " 291 + "TaskView at the current page index was missing.", 292 writer); 293 break; 294 case QUICK_SWITCH_FROM_HOME_FAILED: 295 errorDetected |= printErrorIfTrue( 296 true, 297 prefix, 298 /* errorMessage= */ "Quick switch from home failed: the TaskViews at " 299 + "the current page index and index 0 were missing.", 300 writer); 301 break; 302 case NAVIGATION_MODE_SWITCHED: 303 errorDetected |= printErrorIfTrue( 304 true, 305 prefix, 306 /* errorMessage= */ "Navigation mode switched mid-gesture.", 307 writer); 308 break; 309 case EXPECTING_TASK_APPEARED: 310 case MOTION_DOWN: 311 case SET_END_TARGET: 312 case SET_END_TARGET_HOME: 313 case SET_END_TARGET_ALL_APPS: 314 case SET_END_TARGET_NEW_TASK: 315 case SET_ON_PAGE_TRANSITION_END_CALLBACK: 316 case CANCEL_CURRENT_ANIMATION: 317 case FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER: 318 case STATE_GESTURE_STARTED: 319 case STATE_END_TARGET_ANIMATION_FINISHED: 320 case STATE_CAPTURE_SCREENSHOT: 321 case STATE_HANDLER_INVALIDATED: 322 case STATE_LAUNCHER_DRAWN: 323 default: 324 // No-Op 325 } 326 } 327 328 // Check that all required events were found. 329 errorDetected |= printErrorIfTrue( 330 !encounteredEvents.contains(GestureEvent.MOTION_DOWN), 331 prefix, 332 /* errorMessage= */ "Motion down never detected.", 333 writer); 334 errorDetected |= printErrorIfTrue( 335 !encounteredEvents.contains(GestureEvent.MOTION_UP), 336 prefix, 337 /* errorMessage= */ "Motion up never detected.", 338 writer); 339 340 errorDetected |= printErrorIfTrue( 341 /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) 342 && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET), 343 prefix, 344 /* errorMessage= */ "setEndTarget was called, but " 345 + "onSettledOnEndTarget wasn't.", 346 writer); 347 errorDetected |= printErrorIfTrue( 348 /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) 349 && !encounteredEvents.contains( 350 GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED), 351 prefix, 352 /* errorMessage= */ "setEndTarget was called, but " 353 + "STATE_END_TARGET_ANIMATION_FINISHED was never set.", 354 writer); 355 errorDetected |= printErrorIfTrue( 356 /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) 357 && !encounteredEvents.contains( 358 GestureEvent.STATE_RECENTS_SCROLLING_FINISHED), 359 prefix, 360 /* errorMessage= */ "setEndTarget was called, but " 361 + "STATE_RECENTS_SCROLLING_FINISHED was never set.", 362 writer); 363 errorDetected |= printErrorIfTrue( 364 /* condition= */ encounteredEvents.contains( 365 GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED) 366 && encounteredEvents.contains( 367 GestureEvent.STATE_RECENTS_SCROLLING_FINISHED) 368 && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET), 369 prefix, 370 /* errorMessage= */ "STATE_END_TARGET_ANIMATION_FINISHED and " 371 + "STATE_RECENTS_SCROLLING_FINISHED were set, but onSettledOnEndTarget " 372 + "wasn't called.", 373 writer); 374 375 errorDetected |= printErrorIfTrue( 376 /* condition= */ encounteredEvents.contains( 377 GestureEvent.START_RECENTS_ANIMATION) 378 && !encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION) 379 && !encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION), 380 prefix, 381 /* errorMessage= */ "startRecentsAnimation was called, but " 382 + "finishRecentsAnimation and cancelRecentsAnimation weren't.", 383 writer); 384 385 errorDetected |= printErrorIfTrue( 386 /* condition= */ encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED) 387 && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_COMPLETED) 388 && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_CANCELLED), 389 prefix, 390 /* errorMessage= */ "STATE_GESTURE_STARTED was set, but " 391 + "STATE_GESTURE_COMPLETED and STATE_GESTURE_CANCELLED weren't.", 392 writer); 393 394 errorDetected |= printErrorIfTrue( 395 /* condition= */ encounteredEvents.contains( 396 GestureEvent.STATE_CAPTURE_SCREENSHOT) 397 && !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED), 398 prefix, 399 /* errorMessage= */ "STATE_CAPTURE_SCREENSHOT was set, but " 400 + "STATE_SCREENSHOT_CAPTURED wasn't.", 401 writer); 402 403 errorDetected |= printErrorIfTrue( 404 /* condition= */ encounteredEvents.contains( 405 GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK) 406 && !encounteredEvents.contains( 407 GestureEvent.STATE_RECENTS_SCROLLING_FINISHED), 408 prefix, 409 /* errorMessage= */ "setOnPageTransitionEndCallback called, but " 410 + "STATE_RECENTS_SCROLLING_FINISHED wasn't set.", 411 writer); 412 413 errorDetected |= printErrorIfTrue( 414 /* condition= */ encounteredEvents.contains( 415 GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER) 416 && !encounteredEvents.contains(GestureEvent.CANCEL_CURRENT_ANIMATION) 417 && !encounteredEvents.contains(GestureEvent.STATE_HANDLER_INVALIDATED), 418 prefix, 419 /* errorMessage= */ "AbsSwipeUpHandler.cancelCurrentAnimation " 420 + "wasn't called and STATE_HANDLER_INVALIDATED wasn't set.", 421 writer); 422 423 errorDetected |= printErrorIfTrue( 424 /* condition= */ encounteredEvents.contains( 425 GestureEvent.STATE_RECENTS_ANIMATION_CANCELED) 426 && !encounteredEvents.contains(GestureEvent.CLEANUP_SCREENSHOT), 427 prefix, 428 /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED was set but " 429 + "the task screenshot wasn't cleaned up.", 430 writer); 431 432 errorDetected |= printErrorIfTrue( 433 /* condition= */ encounteredEvents.contains( 434 GestureEvent.EXPECTING_TASK_APPEARED) 435 && !encounteredEvents.contains(GestureEvent.TASK_APPEARED), 436 prefix, 437 /* errorMessage= */ "onTaskAppeared was expected to be called but wasn't.", 438 writer); 439 440 errorDetected |= printErrorIfTrue( 441 /* condition= */ encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION) 442 && !encounteredEvents.contains(GestureEvent.ON_START_RECENTS_ANIMATION), 443 prefix, 444 /* errorMessage= */ 445 "startRecentAnimation was called but onAnimationStart callback was not", 446 writer); 447 errorDetected |= printErrorIfTrue( 448 /* condition= */ 449 encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION) 450 && !encounteredEvents.contains(GestureEvent.ON_FINISH_RECENTS_ANIMATION), 451 prefix, 452 /* errorMessage= */ 453 "finishController was called but onAnimationFinished callback was not", 454 writer); 455 errorDetected |= printErrorIfTrue( 456 /* condition= */ 457 encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION) 458 && !encounteredEvents.contains(GestureEvent.ON_CANCEL_RECENTS_ANIMATION), 459 prefix, 460 /* errorMessage= */ 461 "onRecentsAnimationCanceled was called but onAnimationCanceled was not", 462 writer); 463 464 if (!errorDetected) { 465 writer.println(prefix + "\tNo errors detected."); 466 } 467 } 468 printErrorIfTrue( boolean condition, String prefix, String errorMessage, PrintWriter writer)469 private static boolean printErrorIfTrue( 470 boolean condition, String prefix, String errorMessage, PrintWriter writer) { 471 if (!condition) { 472 return false; 473 } 474 writer.println(prefix + "\t- " + errorMessage); 475 476 return true; 477 } 478 } 479