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