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