Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9e1fbcbe authored by Vadim Tryshev's avatar Vadim Tryshev
Browse files

Implementing detector of view position jumps

I.e. when a view's top, right, left or bottom jumps too far between frames.

Flag: N/A
Test: presubmit, local runs
Bug: 286251603
Change-Id: I8f75a5b9ebb213d56db9f9ba1cc31d398fe7e88f
parent 184fa593
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -179,7 +179,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;
+9 −8
Original line number Diff line number Diff line
@@ -72,13 +72,14 @@ abstract class AnomalyDetector {
     *                     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.
     *                     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);
}
+1 −1
Original line number Diff line number Diff line
@@ -106,7 +106,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?
+126 −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 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;
    }
}
+47 −16
Original line number Diff line number Diff line
@@ -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);