mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 23:36:47 +00:00
Merge "View capture analyzer tree walker + Alpha jump detector V1" into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
a7a27fd4ca
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 static com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.diagPathFromRoot;
|
||||
|
||||
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
|
||||
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnomalyDetector;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Anomaly detector that triggers an error when alpha of a view changes too rapidly.
|
||||
* Invisible views are treated as if they had zero alpha.
|
||||
*/
|
||||
final class AlphaJumpDetector extends AnomalyDetector {
|
||||
// Paths of nodes that are excluded from analysis.
|
||||
private static final Collection<String> PATHS_TO_IGNORE = Set.of(
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id"
|
||||
+ "/search_results_list_view|SearchResultSmallIconRow",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id"
|
||||
+ "/search_results_list_view|SearchResultIcon",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|LauncherRecentsView:id/overview_panel|TaskView",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container"
|
||||
+ "|WidgetsRecyclerView:id/primary_widgets_list_view|WidgetsListHeader:id"
|
||||
+ "/widgets_list_header",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container"
|
||||
+ "|WidgetsRecyclerView:id/primary_widgets_list_view"
|
||||
+ "|StickyHeaderLayout$EmptySpaceView",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|SearchContainerView:id/apps_view|AllAppsRecyclerView:id"
|
||||
+ "/apps_list_view|BubbleTextView:id/icon",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|LauncherRecentsView:id/overview_panel|ClearAllButton:id"
|
||||
+ "/clear_all",
|
||||
"DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
|
||||
+ ":id/drag_layer|NexusOverviewActionsView:id/overview_actions_view"
|
||||
+ "|LinearLayout:id/action_buttons"
|
||||
);
|
||||
// Minimal increase or decrease of view's alpha between frames that triggers the error.
|
||||
private static final float ALPHA_JUMP_THRESHOLD = 1f;
|
||||
|
||||
@Override
|
||||
void initializeNode(AnalysisNode info) {
|
||||
// If the parent view ignores alpha jumps, its descendants will too.
|
||||
final boolean parentIgnoreAlphaJumps = info.parent != null && info.parent.ignoreAlphaJumps;
|
||||
info.ignoreAlphaJumps = parentIgnoreAlphaJumps
|
||||
|| PATHS_TO_IGNORE.contains(diagPathFromRoot(info));
|
||||
}
|
||||
|
||||
@Override
|
||||
void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
|
||||
// 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;
|
||||
|
||||
final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
|
||||
if (latestInfo.ignoreAlphaJumps) return;
|
||||
|
||||
final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
|
||||
final float newAlpha = newInfo != null ? newInfo.alpha : 0;
|
||||
final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
|
||||
|
||||
if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
|
||||
throw new AssertionError(
|
||||
String.format(
|
||||
"Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
|
||||
+ ", threshold: %s, view: %s",
|
||||
alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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 static android.view.View.VISIBLE;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.app.viewcapture.data.ExportedData;
|
||||
import com.android.app.viewcapture.data.FrameData;
|
||||
import com.android.app.viewcapture.data.ViewNode;
|
||||
import com.android.app.viewcapture.data.WindowData;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility that analyzes ViewCapture data and finds anomalies such as views appearing or
|
||||
* disappearing without alpha-fading.
|
||||
*/
|
||||
public class ViewCaptureAnalyzer {
|
||||
private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView";
|
||||
|
||||
/**
|
||||
* Detector of one kind of anomaly.
|
||||
*/
|
||||
abstract static class AnomalyDetector {
|
||||
/**
|
||||
* Initializes fields of the node that are specific to the anomaly detected by this
|
||||
* detector.
|
||||
*/
|
||||
abstract void initializeNode(@NonNull AnalysisNode info);
|
||||
|
||||
/**
|
||||
* Detects anomalies by looking at the last occurrence of a view, and the current one.
|
||||
* 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 earlier
|
||||
* frames.
|
||||
* @param frameN number of the current frame.
|
||||
*/
|
||||
abstract void detectAnomalies(
|
||||
@Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN);
|
||||
}
|
||||
|
||||
// All detectors. They will be invoked in the order listed here.
|
||||
private static final Iterable<AnomalyDetector> ANOMALY_DETECTORS = Arrays.asList(
|
||||
new AlphaJumpDetector()
|
||||
);
|
||||
|
||||
// A view from view capture data converted to a form that's convenient for detecting anomalies.
|
||||
static class AnalysisNode {
|
||||
public String className;
|
||||
public String resourceId;
|
||||
public AnalysisNode parent;
|
||||
|
||||
// Window coordinates of the view.
|
||||
public float left;
|
||||
public float top;
|
||||
|
||||
// Visible scale and alpha, build recursively from the ancestor list.
|
||||
public float scaleX;
|
||||
public float scaleY;
|
||||
public float alpha;
|
||||
|
||||
public int frameN;
|
||||
public ViewNode viewCaptureNode;
|
||||
|
||||
public boolean ignoreAlphaJumps;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("window coordinates: (%s, %s), class path from the root: %s",
|
||||
left, top, diagPathFromRoot(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a view capture record and throws an error if an anomaly is found.
|
||||
*/
|
||||
public static void assertNoAnomalies(ExportedData viewCaptureData) {
|
||||
final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
|
||||
|
||||
final int windowDataCount = viewCaptureData.getWindowDataCount();
|
||||
for (int i = 0; i < windowDataCount; ++i) {
|
||||
analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
|
||||
int scrimClassIndex) {
|
||||
// View hash code => Last seen node with this hash code.
|
||||
// The view is added when we analyze the first frame where it's visible.
|
||||
// After that, it gets updated for every frame where it's visible.
|
||||
// As we go though frames, if a view becomes invisible, it stays in the map.
|
||||
final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
|
||||
|
||||
for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
|
||||
analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
|
||||
// Analyze the node tree starting from the root.
|
||||
analyzeView(
|
||||
frame.getNode(),
|
||||
/* parent = */ null,
|
||||
frameN,
|
||||
/* leftShift = */ 0,
|
||||
/* topShift = */ 0,
|
||||
viewCaptureData,
|
||||
lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
|
||||
// Analyze transitions when a view visible in the last frame become invisible in the
|
||||
// current one.
|
||||
for (AnalysisNode info : lastSeenNodes.values()) {
|
||||
if (info.frameN == frameN - 1) {
|
||||
if (!info.viewCaptureNode.getWillNotDraw()) {
|
||||
ANOMALY_DETECTORS.forEach(
|
||||
detector -> detector.detectAnomalies(
|
||||
/* oldInfo = */ info,
|
||||
/* newInfo = */ null,
|
||||
frameN));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN,
|
||||
float leftShift, float topShift, ExportedData viewCaptureData,
|
||||
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
|
||||
// Skip analysis of invisible views
|
||||
final float parentAlpha = parent != null ? parent.alpha : 1;
|
||||
final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
|
||||
if (alpha <= 0.0) return;
|
||||
|
||||
// Calculate analysis node parameters
|
||||
final int hashcode = viewCaptureNode.getHashcode();
|
||||
final int classIndex = viewCaptureNode.getClassnameIndex();
|
||||
|
||||
final float parentScaleX = parent != null ? parent.scaleX : 1;
|
||||
final float parentScaleY = parent != null ? parent.scaleY : 1;
|
||||
final float scaleX = parentScaleX * viewCaptureNode.getScaleX();
|
||||
final float scaleY = parentScaleY * viewCaptureNode.getScaleY();
|
||||
|
||||
final float left = leftShift
|
||||
+ (viewCaptureNode.getLeft() + viewCaptureNode.getTranslationX()) * parentScaleX
|
||||
+ viewCaptureNode.getWidth() * (parentScaleX - scaleX) / 2;
|
||||
final float top = topShift
|
||||
+ (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
|
||||
+ viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
|
||||
|
||||
// Initialize new analysis node
|
||||
final AnalysisNode newAnalysisNode = new AnalysisNode();
|
||||
newAnalysisNode.className = viewCaptureData.getClassname(classIndex);
|
||||
newAnalysisNode.resourceId = viewCaptureNode.getId();
|
||||
newAnalysisNode.parent = parent;
|
||||
newAnalysisNode.left = left;
|
||||
newAnalysisNode.top = top;
|
||||
newAnalysisNode.scaleX = scaleX;
|
||||
newAnalysisNode.scaleY = scaleY;
|
||||
newAnalysisNode.alpha = alpha;
|
||||
newAnalysisNode.frameN = frameN;
|
||||
newAnalysisNode.viewCaptureNode = viewCaptureNode;
|
||||
ANOMALY_DETECTORS.forEach(detector -> detector.initializeNode(newAnalysisNode));
|
||||
|
||||
// Detect anomalies for the view
|
||||
final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
|
||||
if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
|
||||
ANOMALY_DETECTORS.forEach(
|
||||
detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN));
|
||||
}
|
||||
lastSeenNodes.put(hashcode, newAnalysisNode);
|
||||
|
||||
// Enumerate children starting from the topmost one. Stop at ScrimView, if present.
|
||||
final float leftShiftForChildren = left - viewCaptureNode.getScrollX();
|
||||
final float topShiftForChildren = top - viewCaptureNode.getScrollY();
|
||||
for (int i = viewCaptureNode.getChildrenCount() - 1; i >= 0; --i) {
|
||||
final ViewNode child = viewCaptureNode.getChildren(i);
|
||||
|
||||
// Don't analyze anything under scrim view because we don't know whether it's
|
||||
// transparent.
|
||||
if (child.getClassnameIndex() == scrimClassIndex) break;
|
||||
|
||||
analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren,
|
||||
viewCaptureData, lastSeenNodes,
|
||||
scrimClassIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static float getVisibleAlpha(ViewNode node, float parenVisibleAlpha) {
|
||||
return node.getVisibility() == VISIBLE
|
||||
? parenVisibleAlpha * Math.max(0, Math.min(node.getAlpha(), 1))
|
||||
: 0f;
|
||||
}
|
||||
|
||||
private static String classNameToSimpleName(String className) {
|
||||
return className.substring(className.lastIndexOf(".") + 1);
|
||||
}
|
||||
|
||||
static String diagPathFromRoot(AnalysisNode nodeBox) {
|
||||
final StringBuilder path = new StringBuilder(diagPathElement(nodeBox));
|
||||
for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) {
|
||||
path.insert(0, diagPathElement(ancestor) + "|");
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private static String diagPathElement(AnalysisNode nodeBox) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(classNameToSimpleName(nodeBox.className));
|
||||
if (!"NO_ID".equals(nodeBox.resourceId)) sb.append(":" + nodeBox.resourceId);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user