Merge "Listen to rotary input events and report them to FocusEventDebugView." into main
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index bf6e1c4..50ebab7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5627,7 +5627,7 @@
         public static final String SHOW_TOUCHES = "show_touches";
 
         /**
-         * Show key presses and other events dispatched to focused windows on the screen.
+         * Show key presses dispatched to focused windows on the screen.
          * 0 = no
          * 1 = yes
          * @hide
@@ -5635,6 +5635,14 @@
         public static final String SHOW_KEY_PRESSES = "show_key_presses";
 
         /**
+         * Show rotary input dispatched to focused windows on the screen.
+         * 0 = no
+         * 1 = yes
+         * @hide
+         */
+        public static final String SHOW_ROTARY_INPUT = "show_rotary_input";
+
+        /**
          * Log raw orientation data from
          * {@link com.android.server.policy.WindowOrientationListener} for use with the
          * orientationplot.py tool.
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 0dd8569..80cf6c3 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -184,6 +184,7 @@
         VALIDATORS.put(System.POINTER_LOCATION, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.SHOW_TOUCHES, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.SHOW_KEY_PRESSES, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.SHOW_ROTARY_INPUT, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.WINDOW_ORIENTATION_LISTENER_LOG, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.LOCKSCREEN_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.LOCKSCREEN_DISABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 4678559..c697c1f 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -79,6 +79,7 @@
                     Settings.System.SHOW_GTALK_SERVICE_STATUS, // candidate for backup?
                     Settings.System.SHOW_TOUCHES,
                     Settings.System.SHOW_KEY_PRESSES,
+                    Settings.System.SHOW_ROTARY_INPUT,
                     Settings.System.SIP_ADDRESS_ONLY, // value, not a setting
                     Settings.System.SIP_ALWAYS, // value, not a setting
                     Settings.System.SYSTEM_LOCALES, // bug?
diff --git a/services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java b/services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java
new file mode 100644
index 0000000..67c221f
--- /dev/null
+++ b/services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 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.server.input;
+
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.MotionEvent;
+
+import com.android.server.UiThread;
+
+/**
+ * Receives input events before they are dispatched and reports them to FocusEventDebugView.
+ */
+class FocusEventDebugGlobalMonitor extends InputEventReceiver {
+    private final FocusEventDebugView mDebugView;
+
+    FocusEventDebugGlobalMonitor(FocusEventDebugView debugView, InputManagerService service) {
+        super(service.monitorInput("FocusEventDebugGlobalMonitor", Display.DEFAULT_DISPLAY),
+            UiThread.getHandler().getLooper());
+        mDebugView = debugView;
+    }
+
+    @Override
+    public void onInputEvent(InputEvent event) {
+        try {
+            if (event instanceof MotionEvent) {
+                mDebugView.reportMotionEvent((MotionEvent) event);
+            }
+        } finally {
+            finishInputEvent(event, false);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/FocusEventDebugView.java
index fba2aa6..02e0eb0 100644
--- a/services/core/java/com/android/server/input/FocusEventDebugView.java
+++ b/services/core/java/com/android/server/input/FocusEventDebugView.java
@@ -22,17 +22,20 @@
 
 import android.animation.LayoutTransition;
 import android.annotation.AnyThread;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Typeface;
+import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.TypedValue;
 import android.view.Gravity;
-import android.view.InputEvent;
+import android.view.InputDevice;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.RoundedCorner;
 import android.view.View;
 import android.view.WindowInsets;
@@ -64,42 +67,31 @@
     private static final int KEY_VIEW_MIN_WIDTH_DP = 32;
     private static final int KEY_VIEW_TEXT_SIZE_SP = 12;
 
+    private final InputManagerService mService;
     private final int mOuterPadding;
 
     // Tracks all keys that are currently pressed/down.
     private final Map<Pair<Integer /*deviceId*/, Integer /*scanCode*/>, PressedKeyView>
             mPressedKeys = new HashMap<>();
 
-    private final PressedKeyContainer mPressedKeyContainer;
-    private final PressedKeyContainer mPressedModifierContainer;
+    @Nullable
+    private FocusEventDebugGlobalMonitor mFocusEventDebugGlobalMonitor;
+    @Nullable
+    private PressedKeyContainer mPressedKeyContainer;
+    @Nullable
+    private PressedKeyContainer mPressedModifierContainer;
 
-    FocusEventDebugView(Context c) {
+    FocusEventDebugView(Context c, InputManagerService service) {
         super(c);
         setFocusableInTouchMode(true);
 
+        mService = service;
         final var dm = mContext.getResources().getDisplayMetrics();
         mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm);
 
         setOrientation(HORIZONTAL);
         setLayoutDirection(LAYOUT_DIRECTION_RTL);
         setGravity(Gravity.START | Gravity.BOTTOM);
-
-        mPressedKeyContainer = new PressedKeyContainer(mContext);
-        mPressedKeyContainer.setOrientation(HORIZONTAL);
-        mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM);
-        mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR);
-        final var scroller = new HorizontalScrollView(mContext);
-        scroller.addView(mPressedKeyContainer);
-        scroller.setHorizontalScrollBarEnabled(false);
-        scroller.addOnLayoutChangeListener(
-                (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT));
-        scroller.setHorizontalFadingEdgeEnabled(true);
-        addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1));
-
-        mPressedModifierContainer = new PressedKeyContainer(mContext);
-        mPressedModifierContainer.setOrientation(VERTICAL);
-        mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM);
-        addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
     }
 
     @Override
@@ -135,17 +127,82 @@
         return super.dispatchKeyEvent(event);
     }
 
-    /** Report an input event to the debug view. */
     @AnyThread
-    public void reportEvent(InputEvent event) {
-        if (!(event instanceof KeyEvent)) {
-            // TODO: Support non-pointer MotionEvents.
+    public void updateShowKeyPresses(boolean enabled) {
+        post(() -> handleUpdateShowKeyPresses(enabled));
+    }
+
+    @AnyThread
+    public void updateShowRotaryInput(boolean enabled) {
+        post(() -> handleUpdateShowRotaryInput(enabled));
+    }
+
+    private void handleUpdateShowKeyPresses(boolean enabled) {
+        if (enabled == showKeyPresses()) {
             return;
         }
+
+        if (!enabled) {
+            removeView(mPressedKeyContainer);
+            mPressedKeyContainer = null;
+            removeView(mPressedModifierContainer);
+            mPressedModifierContainer = null;
+            return;
+        }
+
+        mPressedKeyContainer = new PressedKeyContainer(mContext);
+        mPressedKeyContainer.setOrientation(HORIZONTAL);
+        mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM);
+        mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR);
+        final var scroller = new HorizontalScrollView(mContext);
+        scroller.addView(mPressedKeyContainer);
+        scroller.setHorizontalScrollBarEnabled(false);
+        scroller.addOnLayoutChangeListener(
+                (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT));
+        scroller.setHorizontalFadingEdgeEnabled(true);
+        addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1));
+
+        mPressedModifierContainer = new PressedKeyContainer(mContext);
+        mPressedModifierContainer.setOrientation(VERTICAL);
+        mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM);
+        addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+    }
+
+    private void handleUpdateShowRotaryInput(boolean enabled) {
+        if (enabled == showRotaryInput()) {
+            return;
+        }
+
+        if (!enabled) {
+            mFocusEventDebugGlobalMonitor.dispose();
+            mFocusEventDebugGlobalMonitor = null;
+            return;
+        }
+
+        mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService);
+    }
+
+    /** Report a key event to the debug view. */
+    @AnyThread
+    public void reportKeyEvent(KeyEvent event) {
         post(() -> handleKeyEvent(KeyEvent.obtain((KeyEvent) event)));
     }
 
+    /** Report a motion event to the debug view. */
+    @AnyThread
+    public void reportMotionEvent(MotionEvent event) {
+        if (event.getSource() != InputDevice.SOURCE_ROTARY_ENCODER) {
+            return;
+        }
+
+        post(() -> handleRotaryInput(MotionEvent.obtain((MotionEvent) event)));
+    }
+
     private void handleKeyEvent(KeyEvent keyEvent) {
+        if (!showKeyPresses()) {
+            return;
+        }
+
         final var identifier = new Pair<>(keyEvent.getDeviceId(), keyEvent.getScanCode());
         final var container = KeyEvent.isModifierKey(keyEvent.getKeyCode())
                 ? mPressedModifierContainer
@@ -185,6 +242,18 @@
         keyEvent.recycle();
     }
 
+    private void handleRotaryInput(MotionEvent motionEvent) {
+        if (!showRotaryInput()) {
+            return;
+        }
+
+        float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
+        // TODO(b/286086154): replace log with visualization.
+        Log.d(TAG, "ROTARY INPUT: " + String.valueOf(scrollAxisValue));
+
+        motionEvent.recycle();
+    }
+
     private static String getLabel(KeyEvent event) {
         switch (event.getKeyCode()) {
             case KeyEvent.KEYCODE_SPACE:
@@ -232,6 +301,16 @@
         return label;
     }
 
+    /** Determine whether to show key presses by checking one of the key-related objects. */
+    private boolean showKeyPresses() {
+        return mPressedKeyContainer != null;
+    }
+
+    /** Determine whether to show rotary input by checking one of the rotary-related objects. */
+    private boolean showRotaryInput() {
+        return mFocusEventDebugGlobalMonitor != null;
+    }
+
     private static class PressedKeyView extends TextView {
 
         private static final ColorFilter sInvertColors = new ColorMatrixColorFilter(new float[]{
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 6241ebb..b8e9d5d 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -390,6 +390,8 @@
     @GuardedBy("mFocusEventDebugViewLock")
     @Nullable
     private FocusEventDebugView mFocusEventDebugView;
+    private boolean mShowKeyPresses = false;
+    private boolean mShowRotaryInput = false;
 
     /** Point of injection for test dependencies. */
     @VisibleForTesting
@@ -2476,7 +2478,7 @@
     private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
         synchronized (mFocusEventDebugViewLock) {
             if (mFocusEventDebugView != null) {
-                mFocusEventDebugView.reportEvent(event);
+                mFocusEventDebugView.reportKeyEvent(event);
             }
         }
         return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
@@ -3396,14 +3398,45 @@
         mWindowManagerCallbacks.notifyPointerLocationChanged(enabled);
     }
 
-    void updateFocusEventDebugViewEnabled(boolean enabled) {
+    void updateShowKeyPresses(boolean enabled) {
+        if (mShowKeyPresses == enabled) {
+            return;
+        }
+
+        mShowKeyPresses = enabled;
+        updateFocusEventDebugViewEnabled();
+
+        synchronized (mFocusEventDebugViewLock) {
+            if (mFocusEventDebugView != null) {
+                mFocusEventDebugView.updateShowKeyPresses(enabled);
+            }
+        }
+    }
+
+    void updateShowRotaryInput(boolean enabled) {
+        if (mShowRotaryInput == enabled) {
+            return;
+        }
+
+        mShowRotaryInput = enabled;
+        updateFocusEventDebugViewEnabled();
+
+        synchronized (mFocusEventDebugViewLock) {
+            if (mFocusEventDebugView != null) {
+                mFocusEventDebugView.updateShowRotaryInput(enabled);
+            }
+        }
+    }
+
+    private void updateFocusEventDebugViewEnabled() {
+        boolean enabled = mShowKeyPresses || mShowRotaryInput;
         FocusEventDebugView view;
         synchronized (mFocusEventDebugViewLock) {
             if (enabled == (mFocusEventDebugView != null)) {
                 return;
             }
             if (enabled) {
-                mFocusEventDebugView = new FocusEventDebugView(mContext);
+                mFocusEventDebugView = new FocusEventDebugView(mContext, this);
                 view = mFocusEventDebugView;
             } else {
                 view = mFocusEventDebugView;
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index cf7c692..aab491e 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -86,7 +86,9 @@
                 Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_TIMEOUT_MS),
                         (reason) -> updateKeyRepeatInfo(getLatestLongPressTimeoutValue())),
                 Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_DELAY_MS),
-                        (reason) -> updateKeyRepeatInfo(getLatestLongPressTimeoutValue())));
+                        (reason) -> updateKeyRepeatInfo(getLatestLongPressTimeoutValue())),
+                Map.entry(Settings.System.getUriFor(Settings.System.SHOW_ROTARY_INPUT),
+                        (reason) -> updateShowRotaryInput()));
     }
 
     /**
@@ -164,8 +166,11 @@
     }
 
     private void updateShowKeyPresses() {
-        mService.updateFocusEventDebugViewEnabled(
-                getBoolean(Settings.System.SHOW_KEY_PRESSES, false));
+        mService.updateShowKeyPresses(getBoolean(Settings.System.SHOW_KEY_PRESSES, false));
+    }
+
+    private void updateShowRotaryInput() {
+        mService.updateShowRotaryInput(getBoolean(Settings.System.SHOW_ROTARY_INPUT, false));
     }
 
     private void updateAccessibilityLargePointer() {