• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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