Merge "Guarding the touchpad visualiser window with the flag and settings" into main
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 1220542..52bf537 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -597,9 +597,6 @@
         mKeyRemapper.systemRunning();
         mPointerIconCache.systemRunning();
         mKeyboardGlyphManager.systemRunning();
-        if (mTouchpadDebugViewController != null) {
-            mTouchpadDebugViewController.systemRunning();
-        }
     }
 
     private void reloadDeviceAliases() {
@@ -3340,6 +3337,13 @@
         }
     }
 
+    void updateTouchpadVisualizerEnabled(boolean enabled) {
+        mNative.setShouldNotifyTouchpadHardwareState(enabled);
+        if (mTouchpadDebugViewController != null) {
+            mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(enabled);
+        }
+    }
+
     void updatePointerLocationEnabled(boolean enabled) {
         mWindowManagerCallbacks.notifyPointerLocationChanged(enabled);
     }
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index ef61d02..835fb72 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -180,7 +180,7 @@
     }
 
     private void updateTouchpadHardwareStateNotificationsEnabled() {
-        mNative.setShouldNotifyTouchpadHardwareState(InputSettings.useTouchpadVisualizer(mContext));
+        mService.updateTouchpadVisualizerEnabled(InputSettings.useTouchpadVisualizer(mContext));
     }
 
     private void updateTouchpadRightClickZoneEnabled() {
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index 1e59167..c28e74a 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -18,12 +18,10 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
-import android.hardware.display.DisplayManager;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Slog;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.WindowManager;
 
@@ -32,7 +30,7 @@
 
 import java.util.Objects;
 
-public class TouchpadDebugViewController {
+public class TouchpadDebugViewController implements InputManager.InputDeviceListener {
 
     private static final String TAG = "TouchpadDebugView";
 
@@ -43,49 +41,61 @@
     private TouchpadDebugView mTouchpadDebugView;
 
     private final InputManagerService mInputManagerService;
+    private boolean mTouchpadVisualizerEnabled = false;
 
     public TouchpadDebugViewController(Context context, Looper looper,
-                                       InputManagerService inputManagerService) {
-        final DisplayManager displayManager = Objects.requireNonNull(
-                context.getSystemService(DisplayManager.class));
-        final Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
-        mContext = context.createDisplayContext(defaultDisplay);
+            InputManagerService inputManagerService) {
+        //TODO(b/363979581): Handle multi-display scenarios
+        mContext = context;
         mHandler = new Handler(looper);
         mInputManagerService = inputManagerService;
     }
 
-    public void systemRunning() {
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
         final InputManager inputManager = Objects.requireNonNull(
                 mContext.getSystemService(InputManager.class));
-        inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
-        for (int deviceId : inputManager.getInputDeviceIds()) {
-            mInputDeviceListener.onInputDeviceAdded(deviceId);
+        InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+
+        if (Objects.requireNonNull(inputDevice).supportsSource(
+                InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)
+                && mTouchpadVisualizerEnabled) {
+            showDebugView(deviceId);
         }
     }
 
-    private final InputManager.InputDeviceListener mInputDeviceListener =
-            new InputManager.InputDeviceListener() {
-                @Override
-                public void onInputDeviceAdded(int deviceId) {
-                    final InputManager inputManager = Objects.requireNonNull(
-                            mContext.getSystemService(InputManager.class));
-                    InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+        hideDebugView(deviceId);
+    }
 
-                    if (Objects.requireNonNull(inputDevice).supportsSource(
-                            InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)) {
-                        showDebugView(deviceId);
-                    }
-                }
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+    }
 
-                @Override
-                public void onInputDeviceRemoved(int deviceId) {
-                    hideDebugView(deviceId);
-                }
-
-                @Override
-                public void onInputDeviceChanged(int deviceId) {
-                }
-            };
+    /**
+     * Notify the controller that the touchpad visualizer setting value has changed.
+     * This must be called from the same looper thread as {@code mHandler}.
+     */
+    public void updateTouchpadVisualizerEnabled(boolean touchpadVisualizerEnabled) {
+        if (mTouchpadVisualizerEnabled == touchpadVisualizerEnabled) {
+            return;
+        }
+        mTouchpadVisualizerEnabled = touchpadVisualizerEnabled;
+        final InputManager inputManager = Objects.requireNonNull(
+                mContext.getSystemService(InputManager.class));
+        if (touchpadVisualizerEnabled) {
+            inputManager.registerInputDeviceListener(this, mHandler);
+            for (int deviceId : inputManager.getInputDeviceIds()) {
+                onInputDeviceAdded(deviceId);
+            }
+        } else {
+            if (mTouchpadDebugView != null) {
+                hideDebugView(mTouchpadDebugView.getTouchpadId());
+            }
+            inputManager.unregisterInputDeviceListener(this);
+        }
+    }
 
     private void showDebugView(int touchpadId) {
         if (mTouchpadDebugView != null) {
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
new file mode 100644
index 0000000..c7ebd3a
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2024 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.debug;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.InputDevice;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.input.InputManagerService;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Build/Install/Run:
+ * atest TouchpadDebugViewControllerTests
+ */
+
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class TouchpadDebugViewControllerTests {
+    private static final int DEVICE_ID = 1000;
+    private static final String TAG = "TouchpadDebugViewController";
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    private Context mContext;
+    private TouchpadDebugViewController mTouchpadDebugViewController;
+    @Mock
+    private InputManager mInputManagerMock;
+    @Mock
+    private InputManagerService mInputManagerServiceMock;
+    @Mock
+    private WindowManager mWindowManagerMock;
+    private TestableLooper mTestableLooper;
+
+    @Before
+    public void setup() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        TestableContext mTestableContext = new TestableContext(mContext);
+        mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock);
+
+        Rect bounds = new Rect(0, 0, 2560, 1600);
+        WindowMetrics metrics = new WindowMetrics(bounds, new WindowInsets(bounds), 1.0f);
+
+        when(mWindowManagerMock.getCurrentWindowMetrics()).thenReturn(metrics);
+
+        unMockTouchpad();
+
+        mTestableLooper = TestableLooper.get(this);
+
+        mTestableContext.addMockSystemService(InputManager.class, mInputManagerMock);
+
+        mTouchpadDebugViewController = new TouchpadDebugViewController(mTestableContext,
+                mTestableLooper.getLooper(), mInputManagerServiceMock);
+    }
+
+    private InputDevice createTouchpadInputDevice(int id) {
+        return new InputDevice.Builder()
+                .setId(id)
+                .setSources(InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)
+                .setName("Test Device " + id)
+                .build();
+    }
+
+    private void mockTouchpad() {
+        when(mInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID});
+        when(mInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn(
+                createTouchpadInputDevice(DEVICE_ID));
+    }
+
+    private void unMockTouchpad() {
+        when(mInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{});
+        when(mInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn(null);
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingDisabled() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, never()).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+    }
+
+    @Test
+    public void settingEnabledWhileNoTouchpadConnected() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true);
+
+        verify(mWindowManagerMock, never()).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingEnabled() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingEnabledThenDisabled() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, times(1)).removeView(any());
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingDisabledThenEnabled() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, never()).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingDisabledThenTouchpadDisconnected() throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, never()).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+
+        unMockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceRemoved(DEVICE_ID);
+
+        verify(mWindowManagerMock, never()).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+    }
+
+    @Test
+    public void touchpadConnectedWhileSettingEnabledThenTouchpadDisconnectedThenSettingDisabled()
+            throws Exception {
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true);
+
+        mockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, never()).removeView(any());
+
+        unMockTouchpad();
+        mTouchpadDebugViewController.onInputDeviceRemoved(DEVICE_ID);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, times(1)).removeView(any());
+
+        mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false);
+
+        verify(mWindowManagerMock, times(1)).addView(any(), any());
+        verify(mWindowManagerMock, times(1)).removeView(any());
+    }
+}