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 static org.junit.Assert.assertTrue; 19 20 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode; 21 22 import java.util.List; 23 24 /** 25 * Anomaly detector that triggers an error when a view flashes, i.e. appears or disappears for a too 26 * short period of time. 27 */ 28 final class FlashDetector extends AnomalyDetector { 29 // Maximum time period of a view visibility or invisibility that is recognized as a flash. 30 private static final int FLASH_DURATION_MS = 300; 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 CONTENT + "LauncherRootView:id/launcher|FloatingIconView", 41 DRAG_LAYER + "LauncherRecentsView:id/overview_panel|TaskView|TextView", 42 DRAG_LAYER 43 + "LauncherAllAppsContainerView:id/apps_view|AllAppsRecyclerView:id" 44 + "/apps_list_view|BubbleTextView:id/icon", 45 CONTENT 46 + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id" 47 + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content" 48 + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell" 49 + "|WidgetCellPreview:id/widget_preview_container|WidgetImageView:id" 50 + "/widget_preview", 51 CONTENT 52 + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id" 53 + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content" 54 + "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell" 55 + "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge", 56 RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView|IconView:id/icon", 57 DRAG_LAYER + "SearchContainerView:id/apps_view", 58 DRAG_LAYER + "LauncherDragView", 59 DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail", 60 DRAG_LAYER 61 + "WidgetsFullSheet|SpringRelativeLayout:id/container|WidgetsRecyclerView:id" 62 + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header", 63 DRAG_LAYER 64 + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container|LinearLayout:id" 65 + "/linear_layout_container|FrameLayout:id/recycler_view_container" 66 + "|FrameLayout:id/widgets_two_pane_sheet_recyclerview|WidgetsRecyclerView:id" 67 + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header" 68 )); 69 70 // Per-AnalysisNode data that's specific to this detector. 71 private static class NodeData { 72 public boolean ignoreFlashes; 73 74 // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is 75 // ignored. 76 // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no 77 // children. 78 public IgnoreNode ignoreNode; 79 } 80 getNodeData(AnalysisNode info)81 private NodeData getNodeData(AnalysisNode info) { 82 return (NodeData) info.detectorsData[detectorOrdinal]; 83 } 84 85 @Override initializeNode(AnalysisNode info)86 void initializeNode(AnalysisNode info) { 87 final NodeData nodeData = new NodeData(); 88 info.detectorsData[detectorOrdinal] = nodeData; 89 90 // If the parent view ignores flashes, its descendants will too. 91 final boolean parentIgnoresFlashes = info.parent != null && getNodeData( 92 info.parent).ignoreFlashes; 93 if (parentIgnoresFlashes) { 94 nodeData.ignoreFlashes = true; 95 return; 96 } 97 98 // Parent view doesn't ignore flashes. 99 // Initialize this AnalysisNode's ignore-node with the corresponding child of the 100 // ignore-node of the parent, if present. 101 final IgnoreNode parentIgnoreNode = info.parent != null 102 ? getNodeData(info.parent).ignoreNode 103 : IGNORED_NODES_ROOT; 104 nodeData.ignoreNode = parentIgnoreNode != null 105 ? parentIgnoreNode.children.get(info.nodeIdentity) : null; 106 // AnalysisNode will be ignored if the corresponding ignore-node is a leaf. 107 nodeData.ignoreFlashes = 108 nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty(); 109 } 110 111 @Override detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long frameTimeNs, int windowSizePx)112 String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, 113 long frameTimeNs, int windowSizePx) { 114 // Should we check when a view was visible for a short period, then its alpha became 0? 115 // Then 'lastVisible' time should be the last one still visible? 116 // Check only transitions of alpha between 0 and 1? 117 118 // If this is the first time ever when we see the view, there have been no flashes yet. 119 if (oldInfo == null) return null; 120 121 // A flash requires a view to go from the full visibility to no-visibility and then back, 122 // or vice versa. 123 // If the last time the view was seen before the current frame, it didn't have full 124 // visibility; no flash can possibly be detected at the current frame. 125 if (oldInfo.alpha < 1) return null; 126 127 final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo; 128 final NodeData nodeData = getNodeData(latestInfo); 129 if (nodeData.ignoreFlashes) return null; 130 131 // Once the view becomes invisible, see for how long it was visible prior to that. If it 132 // was visible only for a short interval of time, it's a flash. 133 if ( 134 // View is invisible in the current frame 135 newInfo == null 136 // When the view became visible last time, it was a transition from 137 // no-visibility to full visibility. 138 && oldInfo.timeBecameVisibleNs != -1) { 139 final long wasVisibleTimeMs = (frameTimeNs - oldInfo.timeBecameVisibleNs) / 1000000; 140 141 if (wasVisibleTimeMs <= FLASH_DURATION_MS) { 142 nodeData.ignoreFlashes = true; // No need to report flashes in children. 143 return 144 String.format( 145 "View was visible for a too short period of time %dms, which is a" 146 + " flash", 147 wasVisibleTimeMs 148 ); 149 } 150 } 151 152 // Once a view becomes visible, see for how long it was invisible prior to that. If it 153 // was invisible only for a short interval of time, it's a flash. 154 if ( 155 // The view is fully visible now 156 newInfo != null && newInfo.alpha >= 1 157 // The view wasn't visible in the previous frame 158 && frameN != oldInfo.frameN + 1) { 159 // We can assert the below condition because at this point, we know that 160 // oldInfo.alpha >= 1, i.e. it disappeared abruptly. 161 assertTrue("oldInfo.timeBecameInvisibleNs must not be -1", 162 oldInfo.timeBecameInvisibleNs != -1); 163 164 final long wasInvisibleTimeMs = (frameTimeNs - oldInfo.timeBecameInvisibleNs) / 1000000; 165 if (wasInvisibleTimeMs <= FLASH_DURATION_MS) { 166 nodeData.ignoreFlashes = true; // No need to report flashes in children. 167 return 168 String.format( 169 "View was invisible for a too short period of time %dms, which " 170 + "is a flash", 171 wasInvisibleTimeMs); 172 } 173 } 174 return null; 175 } 176 } 177