Merge "[CD Cursor] Add DisplayTopologyValidator" into main
diff --git a/include/input/DisplayTopologyGraph.h b/include/input/DisplayTopologyGraph.h
index 3ae865a..ce7259e 100644
--- a/include/input/DisplayTopologyGraph.h
+++ b/include/input/DisplayTopologyGraph.h
@@ -55,6 +55,8 @@
ui::LogicalDisplayId primaryDisplayId = ui::LogicalDisplayId::INVALID;
std::unordered_map<ui::LogicalDisplayId, std::vector<DisplayTopologyAdjacentDisplay>> graph;
std::unordered_map<ui::LogicalDisplayId, int> displaysDensity;
+
+ bool isValid() const;
};
} // namespace android
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index dc03878..52e0276 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -225,6 +225,7 @@
srcs: [
"AccelerationCurve.cpp",
"CoordinateFilter.cpp",
+ "DisplayTopologyGraph.cpp",
"Input.cpp",
"InputConsumer.cpp",
"InputConsumerNoResampling.cpp",
diff --git a/libs/input/DisplayTopologyGraph.cpp b/libs/input/DisplayTopologyGraph.cpp
new file mode 100644
index 0000000..7ad9f16
--- /dev/null
+++ b/libs/input/DisplayTopologyGraph.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "DisplayTopologyValidator"
+
+#include <android-base/logging.h>
+#include <ftl/enum.h>
+#include <input/DisplayTopologyGraph.h>
+#include <ui/LogicalDisplayId.h>
+
+#include <algorithm>
+
+namespace android {
+
+namespace {
+DisplayTopologyPosition getOppositePosition(DisplayTopologyPosition position) {
+ switch (position) {
+ case DisplayTopologyPosition::LEFT:
+ return DisplayTopologyPosition::RIGHT;
+ case DisplayTopologyPosition::TOP:
+ return DisplayTopologyPosition::BOTTOM;
+ case DisplayTopologyPosition::RIGHT:
+ return DisplayTopologyPosition::LEFT;
+ case DisplayTopologyPosition::BOTTOM:
+ return DisplayTopologyPosition::TOP;
+ }
+}
+
+bool validatePrimaryDisplay(const android::DisplayTopologyGraph& displayTopologyGraph) {
+ return displayTopologyGraph.primaryDisplayId != ui::LogicalDisplayId::INVALID &&
+ displayTopologyGraph.graph.contains(displayTopologyGraph.primaryDisplayId);
+}
+
+bool validateTopologyGraph(const android::DisplayTopologyGraph& displayTopologyGraph) {
+ for (const auto& [sourceDisplay, adjacentDisplays] : displayTopologyGraph.graph) {
+ for (const DisplayTopologyAdjacentDisplay& adjacentDisplay : adjacentDisplays) {
+ const auto adjacentGraphIt = displayTopologyGraph.graph.find(adjacentDisplay.displayId);
+ if (adjacentGraphIt == displayTopologyGraph.graph.end()) {
+ LOG(ERROR) << "Missing adjacent display in topology graph: "
+ << adjacentDisplay.displayId << " for source " << sourceDisplay;
+ return false;
+ }
+ const auto reverseEdgeIt =
+ std::find_if(adjacentGraphIt->second.begin(), adjacentGraphIt->second.end(),
+ [sourceDisplay](const DisplayTopologyAdjacentDisplay&
+ reverseAdjacentDisplay) {
+ return sourceDisplay == reverseAdjacentDisplay.displayId;
+ });
+ if (reverseEdgeIt == adjacentGraphIt->second.end()) {
+ LOG(ERROR) << "Missing reverse edge in topology graph for: " << sourceDisplay
+ << " -> " << adjacentDisplay.displayId;
+ return false;
+ }
+ DisplayTopologyPosition expectedPosition =
+ getOppositePosition(adjacentDisplay.position);
+ if (reverseEdgeIt->position != expectedPosition) {
+ LOG(ERROR) << "Unexpected reverse edge for: " << sourceDisplay << " -> "
+ << adjacentDisplay.displayId
+ << " expected position: " << ftl::enum_string(expectedPosition)
+ << " actual " << ftl::enum_string(reverseEdgeIt->position);
+ return false;
+ }
+ if (reverseEdgeIt->offsetDp != -adjacentDisplay.offsetDp) {
+ LOG(ERROR) << "Unexpected reverse edge offset: " << sourceDisplay << " -> "
+ << adjacentDisplay.displayId
+ << " expected offset: " << -adjacentDisplay.offsetDp << " actual "
+ << reverseEdgeIt->offsetDp;
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool validateDensities(const android::DisplayTopologyGraph& displayTopologyGraph) {
+ for (const auto& [sourceDisplay, adjacentDisplays] : displayTopologyGraph.graph) {
+ if (!displayTopologyGraph.displaysDensity.contains(sourceDisplay)) {
+ LOG(ERROR) << "Missing density value in topology graph for display: " << sourceDisplay;
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace
+
+bool DisplayTopologyGraph::isValid() const {
+ return validatePrimaryDisplay(*this) && validateTopologyGraph(*this) &&
+ validateDensities(*this);
+}
+
+} // namespace android
diff --git a/libs/input/input_flags.aconfig b/libs/input/input_flags.aconfig
index 1c0c9e7..983bbde 100644
--- a/libs/input/input_flags.aconfig
+++ b/libs/input/input_flags.aconfig
@@ -239,3 +239,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_display_topology_validation"
+ namespace: "input"
+ description: "Set to true to enable display topology validation"
+ bug: "401219231"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 18d47f6..677cf1e 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -52,6 +52,7 @@
"AnrTracker_test.cpp",
"CapturedTouchpadEventConverter_test.cpp",
"CursorInputMapper_test.cpp",
+ "DisplayTopologyGraph_test.cpp",
"EventHub_test.cpp",
"FakeEventHub.cpp",
"FakeInputReaderPolicy.cpp",
diff --git a/services/inputflinger/tests/DisplayTopologyGraph_test.cpp b/services/inputflinger/tests/DisplayTopologyGraph_test.cpp
new file mode 100644
index 0000000..fd2f21c
--- /dev/null
+++ b/services/inputflinger/tests/DisplayTopologyGraph_test.cpp
@@ -0,0 +1,120 @@
+/*
+ * 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 <gtest/gtest.h>
+#include <input/DisplayTopologyGraph.h>
+
+#include <string>
+#include <string_view>
+#include <tuple>
+
+namespace android {
+
+namespace {
+
+constexpr ui::LogicalDisplayId DISPLAY_ID_1{1};
+constexpr ui::LogicalDisplayId DISPLAY_ID_2{2};
+constexpr int DENSITY_MEDIUM = 160;
+
+} // namespace
+
+using DisplayTopologyGraphTestFixtureParam =
+ std::tuple<std::string_view /*name*/, DisplayTopologyGraph, bool /*isValid*/>;
+
+class DisplayTopologyGraphTestFixture
+ : public testing::Test,
+ public testing::WithParamInterface<DisplayTopologyGraphTestFixtureParam> {};
+
+TEST_P(DisplayTopologyGraphTestFixture, DisplayTopologyGraphTest) {
+ const auto& [_, displayTopology, isValid] = GetParam();
+ EXPECT_EQ(isValid, displayTopology.isValid());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ DisplayTopologyGraphTest, DisplayTopologyGraphTestFixture,
+ testing::Values(
+ std::make_tuple(
+ "InvalidPrimaryDisplay",
+ DisplayTopologyGraph{.primaryDisplayId = ui::LogicalDisplayId::INVALID,
+ .graph = {},
+ .displaysDensity = {}},
+ false),
+ std::make_tuple("PrimaryDisplayNotInGraph",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {},
+ .displaysDensity = {}},
+ false),
+ std::make_tuple("DisplayDensityMissing",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1, {}}},
+ .displaysDensity = {}},
+ false),
+ std::make_tuple("ValidSingleDisplayTopology",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1, {}}},
+ .displaysDensity = {{DISPLAY_ID_1,
+ DENSITY_MEDIUM}}},
+ true),
+ std::make_tuple(
+ "MissingReverseEdge",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1,
+ {{DISPLAY_ID_2,
+ DisplayTopologyPosition::TOP, 0}}}},
+ .displaysDensity = {{DISPLAY_ID_1, DENSITY_MEDIUM},
+ {DISPLAY_ID_2, DENSITY_MEDIUM}}},
+ false),
+ std::make_tuple(
+ "IncorrectReverseEdgeDirection",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1,
+ {{DISPLAY_ID_2,
+ DisplayTopologyPosition::TOP, 0}}},
+ {DISPLAY_ID_2,
+ {{DISPLAY_ID_1,
+ DisplayTopologyPosition::TOP, 0}}}},
+ .displaysDensity = {{DISPLAY_ID_1, DENSITY_MEDIUM},
+ {DISPLAY_ID_2, DENSITY_MEDIUM}}},
+ false),
+ std::make_tuple(
+ "IncorrectReverseEdgeOffset",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1,
+ {{DISPLAY_ID_2,
+ DisplayTopologyPosition::TOP, 10}}},
+ {DISPLAY_ID_2,
+ {{DISPLAY_ID_1,
+ DisplayTopologyPosition::BOTTOM, 20}}}},
+ .displaysDensity = {{DISPLAY_ID_1, DENSITY_MEDIUM},
+ {DISPLAY_ID_2, DENSITY_MEDIUM}}},
+ false),
+ std::make_tuple(
+ "ValidMultiDisplayTopology",
+ DisplayTopologyGraph{.primaryDisplayId = DISPLAY_ID_1,
+ .graph = {{DISPLAY_ID_1,
+ {{DISPLAY_ID_2,
+ DisplayTopologyPosition::TOP, 10}}},
+ {DISPLAY_ID_2,
+ {{DISPLAY_ID_1,
+ DisplayTopologyPosition::BOTTOM, -10}}}},
+ .displaysDensity = {{DISPLAY_ID_1, DENSITY_MEDIUM},
+ {DISPLAY_ID_2, DENSITY_MEDIUM}}},
+ true)),
+ [](const testing::TestParamInfo<DisplayTopologyGraphTestFixtureParam>& p) {
+ return std::string{std::get<0>(p.param)};
+ });
+
+} // namespace android