1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.util.viewcapture_analysis;
17 
18 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
19 
20 import java.util.List;
21 
22 /**
23  * Anomaly detector that triggers an error when a view position jumps.
24  */
25 final class PositionJumpDetector extends AnomalyDetector {
26     // Maximum allowed jump in "milliwindows", i.e. a 1/1000's of the maximum of the window
27     // dimensions.
28     private static final float JUMP_MIW = 250;
29 
30     private static final String[] BORDER_NAMES = {"left", "top", "right", "bottom"};
31 
32     // Commonly used parts of the paths to ignore.
33     private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
34     private static final String DRAG_LAYER =
35             CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
36     private static final String RECENTS_DRAG_LAYER =
37             CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
38 
39     private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
40             DRAG_LAYER + "SearchContainerView:id/apps_view",
41             DRAG_LAYER + "AppWidgetResizeFrame",
42             DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
43             CONTENT
44                     + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
45                     + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
46             DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
47             DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
48             DRAG_LAYER + "LauncherDragView",
49             RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
50             CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
51             DRAG_LAYER + "FloatingTaskView",
52             DRAG_LAYER + "LauncherRecentsView:id/overview_panel"
53     ));
54 
55     // Per-AnalysisNode data that's specific to this detector.
56     private static class NodeData {
57         public boolean ignoreJumps;
58 
59         // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
60         // ignored.
61         // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
62         // children.
63         public IgnoreNode ignoreNode;
64     }
65 
getNodeData(AnalysisNode info)66     private NodeData getNodeData(AnalysisNode info) {
67         return (NodeData) info.detectorsData[detectorOrdinal];
68     }
69 
70     @Override
initializeNode(AnalysisNode info)71     void initializeNode(AnalysisNode info) {
72         final NodeData nodeData = new NodeData();
73         info.detectorsData[detectorOrdinal] = nodeData;
74 
75         // If the parent view ignores jumps, its descendants will too.
76         final boolean parentIgnoresJumps = info.parent != null && getNodeData(
77                 info.parent).ignoreJumps;
78         if (parentIgnoresJumps) {
79             nodeData.ignoreJumps = true;
80             return;
81         }
82 
83         // Parent view doesn't ignore jumps.
84         // Initialize this AnalysisNode's ignore-node with the corresponding child of the
85         // ignore-node of the parent, if present.
86         final IgnoreNode parentIgnoreNode = info.parent != null
87                 ? getNodeData(info.parent).ignoreNode
88                 : IGNORED_NODES_ROOT;
89         nodeData.ignoreNode = parentIgnoreNode != null
90                 ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
91         // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
92         nodeData.ignoreJumps =
93                 nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
94     }
95 
96     @Override
detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long frameTimeNs, int windowSizePx)97     String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
98             long frameTimeNs, int windowSizePx) {
99         // If the view is not present in the current frame, there can't be a jump detected in the
100         // current frame.
101         if (newInfo == null) return null;
102 
103         // We only detect position jumps if the view was visible in the previous frame.
104         if (oldInfo == null || frameN != oldInfo.frameN + 1) return null;
105 
106         final NodeData newNodeData = getNodeData(newInfo);
107         if (newNodeData.ignoreJumps) return null;
108 
109         final float[] positionDiffs = {
110                 newInfo.left - oldInfo.left,
111                 newInfo.top - oldInfo.top,
112                 newInfo.right - oldInfo.right,
113                 newInfo.bottom - oldInfo.bottom
114         };
115 
116         for (int i = 0; i < 4; ++i) {
117             final float positionDiffAbs = Math.abs(positionDiffs[i]);
118             if (positionDiffAbs * 1000 > JUMP_MIW * windowSizePx) {
119                 newNodeData.ignoreJumps = true;
120                 return String.format("Position jump: %s jumped by %s",
121                         BORDER_NAMES[i], positionDiffAbs);
122             }
123         }
124         return null;
125     }
126 }
127