mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 02:38:20 +00:00
Merge "Implementing detector of view position jumps" into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
1dcfdc3b3f
@@ -180,7 +180,8 @@ final class AlphaJumpDetector extends AnomalyDetector {
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
@@ -68,17 +68,18 @@ abstract class AnomalyDetector {
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ final class FlashDetector extends AnomalyDetector {
|
||||
|
||||
@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?
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,8 @@ public class ViewCaptureAnalyzer {
|
||||
// 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 @@ public class ViewCaptureAnalyzer {
|
||||
// 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 @@ public class ViewCaptureAnalyzer {
|
||||
|
||||
@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 @@ public class ViewCaptureAnalyzer {
|
||||
// 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 @@ public class ViewCaptureAnalyzer {
|
||||
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 @@ public class ViewCaptureAnalyzer {
|
||||
/* oldInfo = */ info,
|
||||
/* newInfo = */ null,
|
||||
anomalies,
|
||||
frameTimeNs)
|
||||
frameTimeNs,
|
||||
windowSizePx)
|
||||
);
|
||||
}
|
||||
info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
|
||||
@@ -159,9 +184,9 @@ public class ViewCaptureAnalyzer {
|
||||
|
||||
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 @@ public class ViewCaptureAnalyzer {
|
||||
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 @@ public class ViewCaptureAnalyzer {
|
||||
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 @@ public class ViewCaptureAnalyzer {
|
||||
}
|
||||
|
||||
// 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 @@ public class ViewCaptureAnalyzer {
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user