diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 8dee10aa53..37b3e3a459 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -46,6 +46,10 @@ import static com.android.quickstep.GestureState.STATE_END_TARGET_SET; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED; import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; @@ -100,6 +104,7 @@ import com.android.launcher3.util.WindowBounds; import com.android.quickstep.BaseActivityInterface.AnimationFactory; import com.android.quickstep.GestureState.GestureEndTarget; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; +import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.ActivityInitListener; import com.android.quickstep.util.AnimatorControllerWithResistance; @@ -320,8 +325,21 @@ public abstract class AbsSwipeUpHandler, initStateCallbacks(); } + @Nullable + private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) { + if (stateFlag == STATE_GESTURE_STARTED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_STARTED; + } else if (stateFlag == STATE_GESTURE_COMPLETED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_COMPLETED; + } else if (stateFlag == STATE_GESTURE_CANCELLED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_CANCELLED; + } + return null; + } + private void initStateCallbacks() { - mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0])); + mStateCallback = new MultiStateCallback( + STATE_NAMES.toArray(new String[0]), AbsSwipeUpHandler::getTrackedEventForState); mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, this::onLauncherPresentAndGestureStarted); @@ -800,7 +818,10 @@ public abstract class AbsSwipeUpHandler, public void onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets) { super.onRecentsAnimationStart(controller, targets); - ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "startRecentsAnimationCallback", + /* extras= */ targets.apps.length, + /* gestureEvent= */ START_RECENTS_ANIMATION); mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(mContext, targets); mRecentsAnimationController = controller; mRecentsAnimationTargets = targets; @@ -843,7 +864,9 @@ public abstract class AbsSwipeUpHandler, @Override public void onRecentsAnimationCanceled(HashMap thumbnailDatas) { - ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation"); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "cancelRecentsAnimation", + /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); mActivityInitListener.unregister(); // Cache the recents animation controller so we can defer its cleanup to after having // properly cleaned up the screenshot without accidentally using it. @@ -1010,7 +1033,9 @@ public abstract class AbsSwipeUpHandler, } break; } - ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + endTarget); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "onSettledOnEndTarget " + endTarget, + /* gestureEvent= */ ON_SETTLED_ON_END_TARGET); } /** @return Whether this was the task we were waiting to appear, and thus handled it. */ @@ -1575,7 +1600,10 @@ public abstract class AbsSwipeUpHandler, private void resumeLastTask() { if (mRecentsAnimationController != null) { mRecentsAnimationController.finish(false /* toRecents */, null); - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "finishRecentsAnimation", + /* extras= */ false, + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); } doLogGesture(LAST_TASK, null); reset(); @@ -1779,7 +1807,10 @@ public abstract class AbsSwipeUpHandler, mRecentsAnimationController.finish(true /* toRecents */, () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "finishRecentsAnimation", + /* extras= */ true, + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); } private void finishCurrentTransitionToHome() { @@ -1791,7 +1822,10 @@ public abstract class AbsSwipeUpHandler, finishRecentsControllerToHome( () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "finishRecentsAnimation", + /* extras= */ true, + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView()); } @@ -1979,7 +2013,10 @@ public abstract class AbsSwipeUpHandler, mRecentsAnimationController.finish(false /* toRecents */, null /* onFinishComplete */); mActivityInterface.onLaunchTaskSuccess(); - ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "finishRecentsAnimation", + /* extras= */ false, + /* gestureEvent= */ FINISH_RECENTS_ANIMATION); } } } diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java index acdbbbd130..bc2f55137b 100644 --- a/quickstep/src/com/android/quickstep/GestureState.java +++ b/quickstep/src/com/android/quickstep/GestureState.java @@ -19,6 +19,7 @@ import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKG import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET; import android.annotation.Nullable; import android.annotation.TargetApi; @@ -30,6 +31,7 @@ import com.android.launcher3.statemanager.StatefulActivity; import com.android.launcher3.tracing.GestureStateProto; import com.android.launcher3.tracing.SwipeHandlerProto; import com.android.quickstep.TopTaskTracker.CachedTaskInfo; +import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @@ -153,7 +155,8 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL mHomeIntent = componentObserver.getHomeIntent(); mOverviewIntent = componentObserver.getOverviewIntent(); mActivityInterface = componentObserver.getActivityInterface(); - mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0])); + mStateCallback = new MultiStateCallback( + STATE_NAMES.toArray(new String[0]), GestureState::getTrackedEventForState); mGestureId = gestureId; } @@ -175,10 +178,21 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL mHomeIntent = new Intent(); mOverviewIntent = new Intent(); mActivityInterface = null; - mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0])); + mStateCallback = new MultiStateCallback( + STATE_NAMES.toArray(new String[0]), GestureState::getTrackedEventForState); mGestureId = -1; } + @Nullable + private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) { + if (stateFlag == STATE_END_TARGET_ANIMATION_FINISHED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED; + } else if (stateFlag == STATE_RECENTS_SCROLLING_FINISHED) { + return ActiveGestureErrorDetector.GestureEvent.STATE_RECENTS_SCROLLING_FINISHED; + } + return null; + } + /** * @return whether the gesture state has the provided {@param stateMask} flags set. */ @@ -311,7 +325,9 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL public void setEndTarget(GestureEndTarget target, boolean isAtomic) { mEndTarget = target; mStateCallback.setState(STATE_END_TARGET_SET); - ActiveGestureLog.INSTANCE.addLog("setEndTarget " + mEndTarget); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "setEndTarget " + mEndTarget, + /* gestureEvent= */ SET_END_TARGET); if (isAtomic) { mStateCallback.setState(STATE_END_TARGET_ANIMATION_FINISHED); } diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java index b3875aef52..56e7fb5ea4 100644 --- a/quickstep/src/com/android/quickstep/MultiStateCallback.java +++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java @@ -22,7 +22,12 @@ import android.os.Looper; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.launcher3.config.FeatureFlags; +import com.android.quickstep.util.ActiveGestureErrorDetector; +import com.android.quickstep.util.ActiveGestureLog; import java.util.ArrayList; import java.util.LinkedList; @@ -41,17 +46,29 @@ public class MultiStateCallback { private final SparseArray>> mStateChangeListeners = new SparseArray<>(); + @NonNull private final TrackedEventsMapper mTrackedEventsMapper; + private final String[] mStateNames; private int mState = 0; public MultiStateCallback(String[] stateNames) { + this(stateNames, stateFlag -> null); + } + + public MultiStateCallback( + String[] stateNames, + @NonNull TrackedEventsMapper trackedEventsMapper) { mStateNames = DEBUG_STATES ? stateNames : null; + mTrackedEventsMapper = trackedEventsMapper; } /** * Adds the provided state flags to the global state on the UI thread and executes any callbacks * as a result. + * + * Also tracks the provided gesture events for error detection. Each provided event must be + * associated with one provided state flag. */ public void setStateOnUiThread(int stateFlag) { if (Looper.myLooper() == Looper.getMainLooper()) { @@ -69,7 +86,9 @@ public class MultiStateCallback { Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState)); } - + if (FeatureFlags.ENABLE_GESTURE_ERROR_DETECTION.get()) { + trackGestureEvents(stateFlag); + } final int oldState = mState; mState = mState | stateFlag; @@ -87,6 +106,20 @@ public class MultiStateCallback { notifyStateChangeListeners(oldState); } + private void trackGestureEvents(int stateFlags) { + for (int index = 0; (stateFlags >> index) != 0; index++) { + if ((stateFlags & (1 << index)) == 0) { + continue; + } + ActiveGestureErrorDetector.GestureEvent gestureEvent = + mTrackedEventsMapper.getTrackedEventForState(1 << index); + if (gestureEvent == null) { + continue; + } + ActiveGestureLog.INSTANCE.trackEvent(gestureEvent); + } + } + /** * Adds the provided state flags to the global state and executes any change handlers * as a result. @@ -174,4 +207,7 @@ public class MultiStateCallback { return joiner.toString(); } + public interface TrackedEventsMapper { + @Nullable ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateflag); + } } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java index c60232424d..887fd54fb3 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java @@ -16,6 +16,7 @@ package com.android.quickstep; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION; import android.graphics.Rect; import android.util.ArraySet; @@ -27,6 +28,7 @@ import androidx.annotation.UiThread; import com.android.launcher3.Utilities; import com.android.launcher3.util.Preconditions; +import com.android.quickstep.util.ActiveGestureLog; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @@ -122,6 +124,9 @@ public class RecentsAnimationCallbacks implements @Override public final void onAnimationCanceled(HashMap thumbnailDatas) { Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> { + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "onRecentsAnimationCancelled", + /* gestureEvent= */ CANCEL_RECENTS_ANIMATION); for (RecentsAnimationListener listener : getListeners()) { listener.onRecentsAnimationCanceled(thumbnailDatas); } @@ -132,6 +137,7 @@ public class RecentsAnimationCallbacks implements @Override public void onTasksAppeared(RemoteAnimationTargetCompat[] apps) { Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> { + ActiveGestureLog.INSTANCE.addLog("onTasksAppeared"); for (RecentsAnimationListener listener : getListeners()) { listener.onTasksAppeared(apps); } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java index 48f0557cb1..b6cfbb0bc8 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java @@ -580,12 +580,15 @@ public class RecentsAnimationDeviceState implements DisplayInfoChangeListener { && ((mSystemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0); } + public String getSystemUiStateString() { + return QuickStepContract.getSystemUiStateString(mSystemUiStateFlags); + } + public void dump(PrintWriter pw) { pw.println("DeviceState:"); pw.println(" canStartSystemGesture=" + canStartSystemGesture()); pw.println(" systemUiFlags=" + mSystemUiStateFlags); - pw.println(" systemUiFlagsDesc=" - + QuickStepContract.getSystemUiStateString(mSystemUiStateFlags)); + pw.println(" systemUiFlagsDesc=" + getSystemUiStateString()); pw.println(" assistantAvailable=" + mAssistantAvailable); pw.println(" assistantDisabled=" + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)); diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java index c9db1533ff..d7227780c6 100644 --- a/quickstep/src/com/android/quickstep/TaskUtils.java +++ b/quickstep/src/com/android/quickstep/TaskUtils.java @@ -18,6 +18,8 @@ package com.android.quickstep; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -25,6 +27,8 @@ import android.content.pm.PackageManager; import android.os.UserHandle; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; @@ -47,16 +51,32 @@ public final class TaskUtils { * TODO: remove this once we switch to getting the icon and label from IconCache. */ public static CharSequence getTitle(Context context, Task task) { - UserHandle user = UserHandle.of(task.key.userId); + return getTitle(context, task.key.userId, task.getTopComponent().getPackageName()); + } + + public static CharSequence getTitle( + @NonNull Context context, + @UserIdInt @Nullable Integer userId, + @Nullable String packageName) { + if (userId == null || packageName == null) { + if (userId == null) { + Log.e(TAG, "Failed to get title; missing userId"); + } + if (packageName == null) { + Log.e(TAG, "Failed to get title; missing packageName"); + } + return ""; + } + UserHandle user = UserHandle.of(userId); ApplicationInfo applicationInfo = new PackageManagerHelper(context) - .getApplicationInfo(task.getTopComponent().getPackageName(), user, 0); + .getApplicationInfo(packageName, user, 0); if (applicationInfo == null) { - Log.e(TAG, "Failed to get title for task " + task); + Log.e(TAG, "Failed to get title for userId=" + userId + ", packageName=" + packageName); return ""; } PackageManager packageManager = context.getPackageManager(); return packageManager.getUserBadgedLabel( - applicationInfo.loadLabel(packageManager), user); + applicationInfo.loadLabel(packageManager), user); } public static ComponentKey getLaunchComponentKeyForTask(Task.TaskKey taskKey) { diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java index cfcba4c30e..d4bf5c75aa 100644 --- a/quickstep/src/com/android/quickstep/TopTaskTracker.java +++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java @@ -24,6 +24,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; +import android.annotation.UserIdInt; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; @@ -282,5 +283,16 @@ public class TopTaskTracker extends ISplitScreenListener.Stub implements TaskSta } return result; } + + @UserIdInt + @Nullable + public Integer getUserId() { + return mTopTask == null ? null : mTopTask.userId; + } + + @Nullable + public String getPackageName() { + return mTopTask == null ? null : mTopTask.baseActivity.getPackageName(); + } } } diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 7c22726874..82be3ec811 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -23,6 +23,8 @@ import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.quickstep.GestureState.DEFAULT_STATE; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION; @@ -650,12 +652,17 @@ public class TouchInteractionService extends Service switch (event.getActionMasked()) { case ACTION_DOWN: case ACTION_UP: - ActiveGestureLog.INSTANCE.addLog("onMotionEvent(" - + (int) event.getRawX() + ", " + (int) event.getRawY() + ")", - event.getActionMasked()); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "onMotionEvent(" + (int) event.getRawX() + ", " + + (int) event.getRawY() + "): " + + MotionEvent.actionToString(event.getActionMasked()), + /* gestureEvent= */ event.getActionMasked() == ACTION_DOWN + ? MOTION_DOWN + : MOTION_UP); break; default: - ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked()); + ActiveGestureLog.INSTANCE.addLog("onMotionEvent: " + + MotionEvent.actionToString(event.getActionMasked())); break; } } @@ -702,15 +709,20 @@ public class TouchInteractionService extends Service public GestureState createGestureState(GestureState previousGestureState) { GestureState gestureState = new GestureState(mOverviewComponentObserver, ActiveGestureLog.INSTANCE.incrementLogId()); + TopTaskTracker.CachedTaskInfo taskInfo; if (mTaskAnimationManager.isRecentsAnimationRunning()) { - gestureState.updateRunningTask(previousGestureState.getRunningTask()); + taskInfo = previousGestureState.getRunningTask(); + gestureState.updateRunningTask(taskInfo); gestureState.updateLastStartedTaskId(previousGestureState.getLastStartedTaskId()); gestureState.updatePreviouslyAppearedTaskIds( previousGestureState.getPreviouslyAppearedTaskIds()); } else { - gestureState.updateRunningTask( - TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false)); + taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false); + gestureState.updateRunningTask(taskInfo); } + // Log initial state for the gesture. + ActiveGestureLog.INSTANCE.addLog( + "Current SystemUi state flags= " + mDeviceState.getSystemUiStateString()); return gestureState; } @@ -1190,6 +1202,7 @@ public class TouchInteractionService extends Service private void printAvailableCommands(PrintWriter pw) { pw.println("Available commands:"); pw.println(" clear-touch-log: Clears the touch interaction log"); + pw.println(" print-gesture-log: only prints the ActiveGestureLog dump"); } private void onCommand(PrintWriter pw, LinkedList args) { @@ -1197,6 +1210,8 @@ public class TouchInteractionService extends Service case "clear-touch-log": ActiveGestureLog.INSTANCE.clear(); break; + case "print-gesture-log": + ActiveGestureLog.INSTANCE.dump("", pw); } } diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 92d3d23a0e..7ccd8af97a 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -30,6 +30,7 @@ import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS; import static com.android.launcher3.util.VelocityUtils.PX_PER_MS; +import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; import android.annotation.TargetApi; @@ -359,7 +360,6 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC } private void notifyGestureStarted(boolean isLikelyToStartNewTask) { - ActiveGestureLog.INSTANCE.addLog("startQuickstep"); if (mInteractionHandler == null) { return; } @@ -373,7 +373,9 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC } private void startTouchTrackingForWindowAnimation(long touchTimeMs) { - ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation"); + ActiveGestureLog.INSTANCE.addLog( + /* event= */ "startRecentsAnimation", + /* gestureEvent= */ START_RECENTS_ANIMATION); mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs); mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java index 7899c55c8a..6f35928a9f 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java @@ -35,7 +35,6 @@ import com.android.quickstep.BaseActivityInterface; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.TaskUtils; -import com.android.quickstep.util.ActiveGestureLog; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -91,7 +90,6 @@ public class OverviewInputConsumer, T extends StatefulAct if (!mStartingInActivityBounds) { mActivityInterface.closeOverlay(); TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); - ActiveGestureLog.INSTANCE.addLog("startQuickstep"); } if (mInputMonitor != null) { TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers"); diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java index bde4240a92..b70fe8e03e 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java @@ -32,7 +32,6 @@ import com.android.launcher3.testing.shared.TestProtocol; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; import com.android.quickstep.RecentsAnimationDeviceState; -import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.TriggerSwipeUpTouchTracker; import com.android.systemui.shared.system.InputMonitorCompat; @@ -79,7 +78,6 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer, @Override public void onSwipeUp(boolean wasFling, PointF finalVelocity) { startHomeIntentSafely(mContext, mGestureState.getHomeIntent(), null); - ActiveGestureLog.INSTANCE.addLog("startQuickstep"); BaseActivity activity = BaseDraggingActivity.fromContext(mContext); int state = (mGestureState != null && mGestureState.getEndTarget() != null) ? mGestureState.getEndTarget().containerType diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java new file mode 100644 index 0000000000..78075def8f --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import android.util.ArraySet; + +import androidx.annotation.NonNull; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Set; + +/** + * Utility class for tracking gesture navigation events as they happen, then detecting and reporting + * known issues at log dump time. + */ +public class ActiveGestureErrorDetector { + + public enum GestureEvent { + MOTION_DOWN, MOTION_UP, SET_END_TARGET, ON_SETTLED_ON_END_TARGET, START_RECENTS_ANIMATION, + FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION, STATE_GESTURE_STARTED, + STATE_GESTURE_COMPLETED, STATE_GESTURE_CANCELLED, STATE_END_TARGET_ANIMATION_FINISHED, + STATE_RECENTS_SCROLLING_FINISHED + } + + private ActiveGestureErrorDetector() {} + + protected static void analyseAndDump( + @NonNull String prefix, + @NonNull PrintWriter writer, + List eventLogs) { + writer.println(prefix + "ActiveGestureErrorDetector:"); + for (int i = 0; i < eventLogs.size(); i++) { + ActiveGestureLog.EventLog eventLog = eventLogs.get(i); + if (eventLog == null) { + continue; + } + int gestureId = eventLog.logId; + writer.println(prefix + "\tError messages for gesture ID: " + gestureId); + + boolean errorDetected = false; + // Use a Set since the order is inherently checked in the loop. + final Set encounteredEvents = new ArraySet<>(); + // Set flags and check order of operations. + for (ActiveGestureLog.EventEntry eventEntry : eventLog.eventEntries) { + GestureEvent gestureEvent = eventEntry.getGestureEvent(); + if (gestureEvent == null) { + continue; + } + encounteredEvents.add(gestureEvent); + switch (gestureEvent) { + case MOTION_UP: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.MOTION_DOWN), + /* errorMessage= */ prefix + "\t\tMotion up detected before/without" + + " motion down.", + writer); + break; + case ON_SETTLED_ON_END_TARGET: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.SET_END_TARGET), + /* errorMessage= */ prefix + "\t\tonSettledOnEndTarget called " + + "before/without setEndTarget.", + writer); + break; + case FINISH_RECENTS_ANIMATION: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION), + /* errorMessage= */ prefix + "\t\tfinishRecentsAnimation called " + + "before/without startRecentsAnimation.", + writer); + break; + case CANCEL_RECENTS_ANIMATION: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION), + /* errorMessage= */ prefix + "\t\tcancelRecentsAnimation called " + + "before/without startRecentsAnimation.", + writer); + break; + case STATE_GESTURE_COMPLETED: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.MOTION_UP), + /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set " + + "before/without motion up.", + writer); + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED), + /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set " + + "before/without STATE_GESTURE_STARTED.", + writer); + break; + case STATE_GESTURE_CANCELLED: + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.MOTION_UP), + /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set " + + "before/without motion up.", + writer); + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED), + /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set " + + "before/without STATE_GESTURE_STARTED.", + writer); + break; + case MOTION_DOWN: + case SET_END_TARGET: + case START_RECENTS_ANIMATION: + case STATE_GESTURE_STARTED: + case STATE_END_TARGET_ANIMATION_FINISHED: + case STATE_RECENTS_SCROLLING_FINISHED: + default: + // No-Op + } + } + + // Check that all required events were found. + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.MOTION_DOWN), + /* errorMessage= */ prefix + "\t\tMotion down never detected.", + writer); + errorDetected |= printErrorIfTrue( + !encounteredEvents.contains(GestureEvent.MOTION_UP), + /* errorMessage= */ prefix + "\t\tMotion up never detected.", + writer); + + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) + && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET), + /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but " + + "onSettledOnEndTarget wasn't.", + writer); + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) + && !encounteredEvents.contains( + GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED), + /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but " + + "STATE_END_TARGET_ANIMATION_FINISHED was never set.", + writer); + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET) + && !encounteredEvents.contains( + GestureEvent.STATE_RECENTS_SCROLLING_FINISHED), + /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but " + + "STATE_RECENTS_SCROLLING_FINISHED was never set.", + writer); + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains( + GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED) + && encounteredEvents.contains( + GestureEvent.STATE_RECENTS_SCROLLING_FINISHED) + && !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET), + /* errorMessage= */ prefix + "\t\tSTATE_END_TARGET_ANIMATION_FINISHED and " + + "STATE_RECENTS_SCROLLING_FINISHED were set, but onSettledOnEndTarget " + + "wasn't called.", + writer); + + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains( + GestureEvent.START_RECENTS_ANIMATION) + && !encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION) + && !encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION), + /* errorMessage= */ prefix + "\t\tstartRecentsAnimation was called, but " + + "finishRecentsAnimation and cancelRecentsAnimation weren't.", + writer); + + errorDetected |= printErrorIfTrue( + /* condition= */ encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED) + && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_COMPLETED) + && !encounteredEvents.contains(GestureEvent.STATE_GESTURE_CANCELLED), + /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_STARTED was set, but " + + "STATE_GESTURE_COMPLETED and STATE_GESTURE_CANCELLED weren't.", + writer); + + if (!errorDetected) { + writer.println(prefix + "\t\tNo errors detected."); + } + } + } + + private static boolean printErrorIfTrue( + boolean condition, String errorMessage, PrintWriter writer) { + if (!condition) { + return false; + } + writer.println(errorMessage); + return true; + } +} diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java index be45f63ddb..9f08010b2b 100644 --- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java +++ b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java @@ -16,6 +16,9 @@ package com.android.quickstep.util; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.config.FeatureFlags; import java.io.PrintWriter; import java.text.SimpleDateFormat; @@ -47,6 +50,7 @@ public class ActiveGestureLog { private static final int TYPE_BOOL_TRUE = 3; private static final int TYPE_BOOL_FALSE = 4; private static final int TYPE_INPUT_CONSUMER = 5; + private static final int TYPE_GESTURE_EVENT = 6; private final EventLog[] logs; private int nextIndex; @@ -57,30 +61,73 @@ public class ActiveGestureLog { this.nextIndex = 0; } + /** + * Track the given event for error detection. + * + * @param gestureEvent GestureEvent representing an event during the current gesture's + * execution. + */ + public void trackEvent(@Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + addLog(TYPE_GESTURE_EVENT, "", 0, CompoundString.NO_OP, gestureEvent); + } + public void addLog(String event) { - addLog(TYPE_ONE_OFF, event, 0, CompoundString.NO_OP); + addLog(event, null); } public void addLog(String event, int extras) { - addLog(TYPE_INTEGER, event, extras, CompoundString.NO_OP); + addLog(event, extras, null); } public void addLog(String event, boolean extras) { - addLog(extras ? TYPE_BOOL_TRUE : TYPE_BOOL_FALSE, event, 0, CompoundString.NO_OP); + addLog(event, extras, null); } public void addLog(CompoundString compoundString) { - addLog(TYPE_INPUT_CONSUMER, "", 0, compoundString); + addLog(TYPE_INPUT_CONSUMER, "", 0, compoundString, null); + } + + /** + * Adds a log and track the associated event for error detection. + * + * @param gestureEvent GestureEvent representing the event being logged. + */ + public void addLog( + String event, @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + addLog(TYPE_ONE_OFF, event, 0, CompoundString.NO_OP, gestureEvent); + } + + public void addLog( + String event, + int extras, + @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + addLog(TYPE_INTEGER, event, extras, CompoundString.NO_OP, gestureEvent); + } + + public void addLog( + String event, + boolean extras, + @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { + addLog( + extras ? TYPE_BOOL_TRUE : TYPE_BOOL_FALSE, + event, + 0, + CompoundString.NO_OP, + gestureEvent); } private void addLog( - int type, String event, float extras, @NonNull CompoundString compoundString) { + int type, + String event, + float extras, + CompoundString compoundString, + @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) { EventLog lastEventLog = logs[(nextIndex + logs.length - 1) % logs.length]; if (lastEventLog == null || mCurrentLogId != lastEventLog.logId) { EventLog eventLog = new EventLog(mCurrentLogId); EventEntry eventEntry = new EventEntry(); - eventEntry.update(type, event, extras, compoundString); + eventEntry.update(type, event, extras, compoundString, gestureEvent); eventLog.eventEntries.add(eventEntry); logs[nextIndex] = eventLog; nextIndex = (nextIndex + 1) % logs.length; @@ -95,15 +142,15 @@ public class ActiveGestureLog { ? lastEventEntries.get(lastEventEntries.size() - 2) : null; // Update the last EventEntry if it's a duplicate - if (isEntrySame(lastEntry, type, event, compoundString) - && isEntrySame(secondLastEntry, type, event, compoundString)) { - lastEntry.update(type, event, extras, compoundString); + if (isEntrySame(lastEntry, type, event, compoundString, gestureEvent) + && isEntrySame(secondLastEntry, type, event, compoundString, gestureEvent)) { + lastEntry.update(type, event, extras, compoundString, gestureEvent); secondLastEntry.duplicateCount++; return; } EventEntry eventEntry = new EventEntry(); - eventEntry.update(type, event, extras, compoundString); + eventEntry.update(type, event, extras, compoundString, gestureEvent); lastEventEntries.add(eventEntry); } @@ -115,12 +162,14 @@ public class ActiveGestureLog { writer.println(prefix + "ActiveGestureLog history:"); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSSZ ", Locale.US); Date date = new Date(); + ArrayList eventLogs = new ArrayList<>(); for (int i = 0; i < logs.length; i++) { EventLog eventLog = logs[(nextIndex + logs.length - i - 1) % logs.length]; if (eventLog == null) { continue; } + eventLogs.add(eventLog); writer.println(prefix + "\tLogs for logId: " + eventLog.logId); List eventEntries = eventLog.eventEntries; @@ -146,6 +195,8 @@ public class ActiveGestureLog { case TYPE_INPUT_CONSUMER: msg.append(eventEntry.mCompoundString); break; + case TYPE_GESTURE_EVENT: + continue; default: // fall out } if (eventEntry.duplicateCount > 0) { @@ -154,6 +205,10 @@ public class ActiveGestureLog { writer.println(msg); } } + + if (FeatureFlags.ENABLE_GESTURE_ERROR_DETECTION.get()) { + ActiveGestureErrorDetector.analyseAndDump(prefix + '\t', writer, eventLogs); + } } /** @@ -165,44 +220,59 @@ public class ActiveGestureLog { } private boolean isEntrySame( - EventEntry entry, int type, String event, CompoundString compoundString) { + EventEntry entry, + int type, + String event, + CompoundString compoundString, + ActiveGestureErrorDetector.GestureEvent gestureEvent) { return entry != null && entry.type == type && entry.event.equals(event) - && entry.mCompoundString.equals(compoundString); + && entry.mCompoundString.equals(compoundString) + && entry.gestureEvent == gestureEvent; } /** A single event entry. */ - private static class EventEntry { + protected static class EventEntry { private int type; private String event; private float extras; @NonNull private CompoundString mCompoundString; + private ActiveGestureErrorDetector.GestureEvent gestureEvent; private long time; private int duplicateCount; - public void update( + private EventEntry() {} + + @Nullable + protected ActiveGestureErrorDetector.GestureEvent getGestureEvent() { + return gestureEvent; + } + + private void update( int type, String event, float extras, - @NonNull CompoundString compoundString) { + @NonNull CompoundString compoundString, + ActiveGestureErrorDetector.GestureEvent gestureEvent) { this.type = type; this.event = event; this.extras = extras; this.mCompoundString = compoundString; + this.gestureEvent = gestureEvent; time = System.currentTimeMillis(); duplicateCount = 0; } } /** An entire log of entries associated with a single log ID */ - private static class EventLog { + protected static class EventLog { - private final List eventEntries = new ArrayList<>(); - private final int logId; + protected final List eventEntries = new ArrayList<>(); + protected final int logId; - protected EventLog(int logId) { + private EventLog(int logId) { this.logId = logId; } } diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java index e758f5bcd7..69ed2f8028 100644 --- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java +++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java @@ -194,7 +194,11 @@ public class MotionPauseDetector { } if (mIsPaused != isPaused) { mIsPaused = isPaused; - Log.d(TAG, "onMotionPauseChanged, paused=" + mIsPaused + " reason=" + reason); + String logString = "onMotionPauseChanged, paused=" + mIsPaused + " reason=" + reason; + if (Utilities.IS_RUNNING_IN_TEST_HARNESS) { + Log.d(TAG, logString); + } + ActiveGestureLog.INSTANCE.addLog(logString); boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused; if (mIsPaused) { AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext); diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 005973c06b..82a47b03f4 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -68,6 +68,11 @@ public final class FeatureFlags { false, "Log the reason why an Input Consumer was selected for a gesture."); + public static final BooleanFlag ENABLE_GESTURE_ERROR_DETECTION = getDebugFlag( + "ENABLE_GESTURE_ERROR_DETECTION", + false, + "Analyze gesture events and log detected errors"); + // When enabled the promise icon is visible in all apps while installation an app. public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag( "PROMISE_APPS_IN_ALL_APPS", false, "Add promise icon in all-apps");