[CD Cursor] Devopts override for connected displays flag

Add and use a sysprop based override for connected displays cursor flag.
Override is expected to require an restart on change. Allowing us to
cache its value.

Test: atest inputflinger_tests
Bug: 362719483
Flag: com.android.input.flags.connected_displays_cursor

Change-Id: I8634e61eb57c82b2b4c904239a4d407ce69f97d8
diff --git a/include/input/InputFlags.h b/include/input/InputFlags.h
new file mode 100644
index 0000000..0e194ea
--- /dev/null
+++ b/include/input/InputFlags.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 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.
+ */
+
+#pragma once
+
+namespace android {
+
+class InputFlags {
+public:
+    /**
+     * Check if connected displays feature is enabled, either via the feature flag or settings
+     * override.
+     */
+    static bool connectedDisplaysCursorEnabled();
+};
+
+} // namespace android
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index d2e4320..ff26184 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -230,6 +230,7 @@
         "InputConsumerNoResampling.cpp",
         "InputDevice.cpp",
         "InputEventLabels.cpp",
+        "InputFlags.cpp",
         "InputTransport.cpp",
         "InputVerifier.cpp",
         "KeyCharacterMap.cpp",
diff --git a/libs/input/InputFlags.cpp b/libs/input/InputFlags.cpp
new file mode 100644
index 0000000..555b138
--- /dev/null
+++ b/libs/input/InputFlags.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 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.
+ */
+
+#include <input/InputFlags.h>
+
+#include <android-base/logging.h>
+#include <com_android_input_flags.h>
+#include <cutils/properties.h>
+
+#include <string>
+
+namespace android {
+
+bool InputFlags::connectedDisplaysCursorEnabled() {
+    static std::optional<bool> cachedDevOption;
+    if (!cachedDevOption.has_value()) {
+        char value[PROPERTY_VALUE_MAX];
+        constexpr static auto sysprop_name = "persist.wm.debug.desktop_experience_devopts";
+        const int devOptionEnabled =
+                property_get(sysprop_name, value, nullptr) > 0 ? std::atoi(value) : 0;
+        cachedDevOption = devOptionEnabled == 1;
+    }
+    if (cachedDevOption.value_or(false)) {
+        return true;
+    }
+    return com::android::input::flags::connected_displays_cursor();
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/services/inputflinger/PointerChoreographer.cpp b/services/inputflinger/PointerChoreographer.cpp
index d02e643..85f842c 100644
--- a/services/inputflinger/PointerChoreographer.cpp
+++ b/services/inputflinger/PointerChoreographer.cpp
@@ -23,6 +23,7 @@
 #if defined(__ANDROID__)
 #include <gui/SurfaceComposerClient.h>
 #endif
+#include <input/InputFlags.h>
 #include <input/Keyboard.h>
 #include <input/PrintTools.h>
 #include <unordered_set>
@@ -328,7 +329,7 @@
             filterPointerMotionForAccessibilityLocked(pc.getPosition(), vec2{deltaX, deltaY},
                                                       newArgs.displayId);
     vec2 unconsumedDelta = pc.move(filteredDelta.x, filteredDelta.y);
-    if (com::android::input::flags::connected_displays_cursor() &&
+    if (InputFlags::connectedDisplaysCursorEnabled() &&
         (std::abs(unconsumedDelta.x) > 0 || std::abs(unconsumedDelta.y) > 0)) {
         handleUnconsumedDeltaLocked(pc, unconsumedDelta);
         // pointer may have moved to a different viewport
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index ca6b85d..a77dc43 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -32,6 +32,7 @@
 #include <gui/SurfaceComposerClient.h>
 #endif
 #include <input/InputDevice.h>
+#include <input/InputFlags.h>
 #include <input/PrintTools.h>
 #include <input/TraceTools.h>
 #include <openssl/mem.h>
@@ -7506,8 +7507,7 @@
         return;
     }
 
-    if (com::android::input::flags::connected_displays_cursor() &&
-        isMouseOrTouchpad(entry.source)) {
+    if (InputFlags::connectedDisplaysCursorEnabled() && isMouseOrTouchpad(entry.source)) {
         mCursorStateByDisplay[windowInfos.getPrimaryDisplayId(entry.displayId)] =
                 std::move(touchState);
     } else {
@@ -7518,8 +7518,7 @@
 void InputDispatcher::DispatcherTouchState::eraseTouchStateForMotionEntry(
         const android::inputdispatcher::MotionEntry& entry,
         const DispatcherWindowInfo& windowInfos) {
-    if (com::android::input::flags::connected_displays_cursor() &&
-        isMouseOrTouchpad(entry.source)) {
+    if (InputFlags::connectedDisplaysCursorEnabled() && isMouseOrTouchpad(entry.source)) {
         mCursorStateByDisplay.erase(windowInfos.getPrimaryDisplayId(entry.displayId));
     } else {
         mTouchStatesByDisplay.erase(entry.displayId);
@@ -7529,8 +7528,7 @@
 const TouchState* InputDispatcher::DispatcherTouchState::getTouchStateForMotionEntry(
         const android::inputdispatcher::MotionEntry& entry,
         const DispatcherWindowInfo& windowInfos) const {
-    if (com::android::input::flags::connected_displays_cursor() &&
-        isMouseOrTouchpad(entry.source)) {
+    if (InputFlags::connectedDisplaysCursorEnabled() && isMouseOrTouchpad(entry.source)) {
         auto touchStateIt =
                 mCursorStateByDisplay.find(windowInfos.getPrimaryDisplayId(entry.displayId));
         if (touchStateIt != mCursorStateByDisplay.end()) {
diff --git a/services/inputflinger/dispatcher/InputState.cpp b/services/inputflinger/dispatcher/InputState.cpp
index 5abd65a..d21c4d7 100644
--- a/services/inputflinger/dispatcher/InputState.cpp
+++ b/services/inputflinger/dispatcher/InputState.cpp
@@ -16,6 +16,7 @@
 
 #include "DebugConfig.h"
 #include "input/InputDevice.h"
+#include "input/InputFlags.h"
 
 #include "InputState.h"
 
@@ -234,8 +235,8 @@
 ssize_t InputState::findMotionMemento(const MotionEntry& entry, bool hovering) const {
     // If we have connected displays a mouse can move between displays and displayId may change
     // while a gesture is in-progress.
-    const bool skipDisplayCheck = com::android::input::flags::connected_displays_cursor() &&
-            isMouseOrTouchpad(entry.source);
+    const bool skipDisplayCheck =
+            InputFlags::connectedDisplaysCursorEnabled() && isMouseOrTouchpad(entry.source);
     for (size_t i = 0; i < mMotionMementos.size(); i++) {
         const MotionMemento& memento = mMotionMementos[i];
         if (memento.deviceId == entry.deviceId && memento.source == entry.source &&
@@ -355,7 +356,7 @@
         // it's unlikely that those two streams would be consistent with each other. Therefore,
         // cancel the previous gesture if the display id changes.
         // Except when we have connected-displays where a mouse may move across display boundaries.
-        const bool skipDisplayCheck = (com::android::input::flags::connected_displays_cursor() &&
+        const bool skipDisplayCheck = (InputFlags::connectedDisplaysCursorEnabled() &&
                                        isMouseOrTouchpad(motionEntry.source));
         if (!skipDisplayCheck && motionEntry.displayId != lastMemento.displayId) {
             LOG(INFO) << "Canceling stream: last displayId was " << lastMemento.displayId