Merge "Implementing detector of view position jumps" into udc-qpr-dev am: 1dcfdc3b3f
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/24452947
Change-Id: Ieb3a1f3ee38d795e705b4ee2fdfbc482f0b501e2
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
index 49abad4..4b65439 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -180,7 +180,8 @@
}
@Override
- String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp) {
+ String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp,
+ int windowSizePx) {
// If the view was previously seen, proceed with analysis only if it was present in the
// view hierarchy in the previous frame.
if (oldInfo != null && oldInfo.frameN != frameN) return null;
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
index 09e2f65..786791c 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
@@ -68,17 +68,18 @@
* null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
* If an anomaly is detected, an exception will be thrown.
*
- * @param oldInfo the view, as seen in the last frame that contained it in the view
- * hierarchy before 'currentFrame'. 'null' means that the view is first seen
- * in the 'currentFrame'.
- * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
- * the view is not present in the 'currentFrame', but was present in the previous
- * frame.
- * @param frameN number of the current frame.
+ * @param oldInfo the view, as seen in the last frame that contained it in the view
+ * hierarchy before 'currentFrame'. 'null' means that the view is first seen
+ * in the 'currentFrame'.
+ * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
+ * the view is not present in the 'currentFrame', but was present in the
+ * previous frame.
+ * @param frameN number of the current frame.
+ * @param windowSizePx maximum of the window width and height, in pixels.
* @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
*/
abstract String detectAnomalies(
@Nullable ViewCaptureAnalyzer.AnalysisNode oldInfo,
@Nullable ViewCaptureAnalyzer.AnalysisNode newInfo, int frameN,
- long frameTimeNs);
+ long frameTimeNs, int windowSizePx);
}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
index 6d9198f..8b88ace 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
@@ -110,7 +110,7 @@
@Override
String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
- long frameTimeNs) {
+ long frameTimeNs, int windowSizePx) {
// Should we check when a view was visible for a short period, then its alpha became 0?
// Then 'lastVisible' time should be the last one still visible?
// Check only transitions of alpha between 0 and 1?
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
new file mode 100644
index 0000000..a1ddcb0
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 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.launcher3.util.viewcapture_analysis;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when a view position jumps.
+ */
+final class PositionJumpDetector extends AnomalyDetector {
+ // Maximum allowed jump in "milliwindows", i.e. a 1/1000's of the maximum of the window
+ // dimensions.
+ private static final float JUMP_MIW = 250;
+
+ private static final String[] BORDER_NAMES = {"left", "top", "right", "bottom"};
+
+ // Commonly used parts of the paths to ignore.
+ private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+ private static final String DRAG_LAYER =
+ CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+ private static final String RECENTS_DRAG_LAYER =
+ CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+ private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+ DRAG_LAYER + "SearchContainerView:id/apps_view",
+ DRAG_LAYER + "AppWidgetResizeFrame",
+ DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
+ CONTENT
+ + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
+ DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
+ DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
+ DRAG_LAYER + "LauncherDragView",
+ RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView",
+ CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+ DRAG_LAYER + "FloatingTaskView",
+ DRAG_LAYER + "LauncherRecentsView:id/overview_panel"
+ ));
+
+ // Per-AnalysisNode data that's specific to this detector.
+ private static class NodeData {
+ public boolean ignoreJumps;
+
+ // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+ // ignored.
+ // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+ // children.
+ public IgnoreNode ignoreNode;
+ }
+
+ private NodeData getNodeData(AnalysisNode info) {
+ return (NodeData) info.detectorsData[detectorOrdinal];
+ }
+
+ @Override
+ void initializeNode(AnalysisNode info) {
+ final NodeData nodeData = new NodeData();
+ info.detectorsData[detectorOrdinal] = nodeData;
+
+ // If the parent view ignores jumps, its descendants will too.
+ final boolean parentIgnoresJumps = info.parent != null && getNodeData(
+ info.parent).ignoreJumps;
+ if (parentIgnoresJumps) {
+ nodeData.ignoreJumps = true;
+ return;
+ }
+
+ // Parent view doesn't ignore jumps.
+ // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+ // ignore-node of the parent, if present.
+ final IgnoreNode parentIgnoreNode = info.parent != null
+ ? getNodeData(info.parent).ignoreNode
+ : IGNORED_NODES_ROOT;
+ nodeData.ignoreNode = parentIgnoreNode != null
+ ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+ // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+ nodeData.ignoreJumps =
+ nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+ }
+
+ @Override
+ String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
+ long frameTimeNs, int windowSizePx) {
+ // If the view is not present in the current frame, there can't be a jump detected in the
+ // current frame.
+ if (newInfo == null) return null;
+
+ // We only detect position jumps if the view was visible in the previous frame.
+ if (oldInfo == null || frameN != oldInfo.frameN + 1) return null;
+
+ final NodeData newNodeData = getNodeData(newInfo);
+ if (newNodeData.ignoreJumps) return null;
+
+ final float[] positionDiffs = {
+ newInfo.left - oldInfo.left,
+ newInfo.top - oldInfo.top,
+ newInfo.right - oldInfo.right,
+ newInfo.bottom - oldInfo.bottom
+ };
+
+ for (int i = 0; i < 4; ++i) {
+ final float positionDiffAbs = Math.abs(positionDiffs[i]);
+ if (positionDiffAbs * 1000 > JUMP_MIW * windowSizePx) {
+ newNodeData.ignoreJumps = true;
+ return String.format("Position jump: %s jumped by %s",
+ BORDER_NAMES[i], positionDiffAbs);
+ }
+ }
+ return null;
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
index ccb4a1e..9459cc2 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -36,7 +36,8 @@
// All detectors. They will be invoked in the order listed here.
private static final AnomalyDetector[] ANOMALY_DETECTORS = {
new AlphaJumpDetector(),
- new FlashDetector()
+ new FlashDetector(),
+ new PositionJumpDetector()
};
static {
@@ -52,6 +53,8 @@
// Window coordinates of the view.
public float left;
public float top;
+ public float right;
+ public float bottom;
// Visible scale and alpha, build recursively from the ancestor list.
public float scaleX;
@@ -81,7 +84,8 @@
@Override
public String toString() {
- return String.format("view window coordinates: (%s, %s)", left, top);
+ return String.format("view window coordinates: (%s, %s, %s, %s)",
+ left, top, right, bottom);
}
}
@@ -112,15 +116,33 @@
// As we go though frames, if a view becomes invisible, it stays in the map.
final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
+ int windowWidthPx = -1;
+ int windowHeightPx = -1;
+
for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
- analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
- scrimClassIndex, anomalies);
+ final FrameData frame = windowData.getFrameData(frameN);
+ final ViewNode rootNode = frame.getNode();
+
+ // If the rotation or window size has changed, reset the analyzer state.
+ final boolean isFirstFrame = windowWidthPx != rootNode.getWidth()
+ || windowHeightPx != rootNode.getHeight();
+ if (isFirstFrame) {
+ windowWidthPx = rootNode.getWidth();
+ windowHeightPx = rootNode.getHeight();
+ lastSeenNodes.clear();
+ }
+
+ final int windowSizePx = Math.max(rootNode.getWidth(), rootNode.getHeight());
+
+ analyzeFrame(frameN, isFirstFrame, frame, viewCaptureData, lastSeenNodes,
+ scrimClassIndex, anomalies, windowSizePx);
}
}
- private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
+ private static void analyzeFrame(int frameN, boolean isFirstFrame, FrameData frame,
+ ExportedData viewCaptureData,
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
- Map<String, String> anomalies) {
+ Map<String, String> anomalies, int windowSizePx) {
// Analyze the node tree starting from the root.
long frameTimeNs = frame.getTimestamp();
analyzeView(
@@ -128,12 +150,14 @@
frame.getNode(),
/* parent = */ null,
frameN,
+ isFirstFrame,
/* leftShift = */ 0,
/* topShift = */ 0,
viewCaptureData,
lastSeenNodes,
scrimClassIndex,
- anomalies);
+ anomalies,
+ windowSizePx);
// Analyze transitions when a view visible in the previous frame became invisible in the
// current one.
@@ -148,7 +172,8 @@
/* oldInfo = */ info,
/* newInfo = */ null,
anomalies,
- frameTimeNs)
+ frameTimeNs,
+ windowSizePx)
);
}
info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
@@ -159,9 +184,9 @@
private static void analyzeView(long frameTimeNs, ViewNode viewCaptureNode, AnalysisNode parent,
int frameN,
- float leftShift, float topShift, ExportedData viewCaptureData,
+ boolean isFirstFrame, float leftShift, float topShift, ExportedData viewCaptureData,
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
- Map<String, String> anomalies) {
+ Map<String, String> anomalies, int windowSizePx) {
// Skip analysis of invisible views
final float parentAlpha = parent != null ? parent.alpha : 1;
final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
@@ -182,6 +207,8 @@
final float top = topShift
+ (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
+ viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
+ final float width = viewCaptureNode.getWidth() * scaleX;
+ final float height = viewCaptureNode.getHeight() * scaleY;
// Initialize new analysis node
final AnalysisNode newAnalysisNode = new AnalysisNode();
@@ -192,6 +219,8 @@
newAnalysisNode.parent = parent;
newAnalysisNode.left = left;
newAnalysisNode.top = top;
+ newAnalysisNode.right = left + width;
+ newAnalysisNode.bottom = top + height;
newAnalysisNode.scaleX = scaleX;
newAnalysisNode.scaleY = scaleY;
newAnalysisNode.alpha = alpha;
@@ -216,11 +245,11 @@
}
// Detect anomalies for the view.
- if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
+ if (!isFirstFrame && !viewCaptureNode.getWillNotDraw()) {
Arrays.stream(ANOMALY_DETECTORS).forEach(
detector ->
detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
- anomalies, frameTimeNs)
+ anomalies, frameTimeNs, windowSizePx)
);
}
lastSeenNodes.put(hashcode, newAnalysisNode);
@@ -235,17 +264,19 @@
// transparent.
if (child.getClassnameIndex() == scrimClassIndex) break;
- analyzeView(frameTimeNs, child, newAnalysisNode, frameN, leftShiftForChildren,
+ analyzeView(frameTimeNs, child, newAnalysisNode, frameN, isFirstFrame,
+ leftShiftForChildren,
topShiftForChildren,
- viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies);
+ viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies, windowSizePx);
}
}
private static void detectAnomaly(AnomalyDetector detector, int frameN,
AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
- Map<String, String> anomalies, long frameTimeNs) {
+ Map<String, String> anomalies, long frameTimeNs, int windowSizePx) {
final String maybeAnomaly =
- detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs);
+ detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs,
+ windowSizePx);
if (maybeAnomaly != null) {
AnalysisNode latestInfo = newAnalysisNode != null ? newAnalysisNode : oldAnalysisNode;
final String viewDiagPath = diagPathFromRoot(latestInfo);