• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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