Loading tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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)); } } } tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java 0 → 100644 +238 −0 Original line number Diff line number Diff line /* * 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(); } } Loading
tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java 0 → 100644 +91 −0 Original line number Diff line number Diff line /* * 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)); } } }
tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java 0 → 100644 +238 −0 Original line number Diff line number Diff line /* * 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(); } }