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

Commit 25972b1f authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Adding support for continously capturing view hierarcy in Launcher

Bug: 238243939
Test: Verified data being captured and dumped
Change-Id: Ibe069d39ccf728f7b953f85085e58976be6e05ac
parent 5672b099
Loading
Loading
Loading
Loading
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.
 */

syntax = "proto2";

package com.android.launcher3.view;

option java_outer_classname = "ViewCaptureData";

message ExportedData {

  repeated FrameData frameData = 1;
}

message FrameData {
  optional int64 timestamp = 1;
  optional ViewNode node = 2;
}

message ViewNode {
  optional string classname = 1;
  optional string id = 2;
  optional int32 left = 3;
  optional int32 top = 4;
  optional int32 width = 5;
  optional int32 height = 6;
  optional int32 scrollX = 7;
  optional int32 scrollY = 8;

  optional float translationX = 9;
  optional float translationY = 10;
  optional float scaleX = 11 [default = 1];
  optional float scaleY = 12 [default = 1];
  optional float alpha = 13 [default = 1];

  optional bool willNotDraw = 14;
  optional bool clipChildren = 15;
  optional int32 visibility = 16;

  repeated ViewNode children = 17;
}
+14 −0
Original line number Diff line number Diff line
@@ -194,6 +194,7 @@ import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.util.UiThreadHelper;
import com.android.launcher3.util.ViewCapture;
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.FloatingIconView;
@@ -388,6 +389,7 @@ public class Launcher extends StatefulActivity<LauncherState>
    private LauncherState mPrevLauncherState;

    private StringCache mStringCache;
    private ViewCapture mViewCapture;

    @Override
    @TargetApi(Build.VERSION_CODES.S)
@@ -1478,6 +1480,14 @@ public class Launcher extends StatefulActivity<LauncherState>
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mOverlayManager.onAttachedToWindow();
        if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
            View root = getDragLayer().getRootView();
            if (mViewCapture != null) {
                root.getViewTreeObserver().removeOnDrawListener(mViewCapture);
            }
            mViewCapture = new ViewCapture(root);
            root.getViewTreeObserver().addOnDrawListener(mViewCapture);
        }
    }

    @Override
@@ -2997,6 +3007,10 @@ public class Launcher extends StatefulActivity<LauncherState>
        writer.println(prefix + "\tmRotationHelper: " + mRotationHelper);
        writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening());

        if (mViewCapture != null) {
            writer.println(prefix + "\tmViewCapture: " + mViewCapture.dumpToString());
        }

        // Extra logging for general debugging
        mDragLayer.dump(prefix, writer);
        mStateManager.dump(prefix, writer);
+3 −0
Original line number Diff line number Diff line
@@ -285,6 +285,9 @@ public final class FeatureFlags {
            "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", false,
            "Use local overrides for search request timeout");

    public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag(
            "CONTINUOUS_VIEW_TREE_CAPTURE", false, "Capture View tree every frame");

    public static void initialize(Context context) {
        synchronized (sDebugFlags) {
            for (DebugFlag flag : sDebugFlags) {
+212 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnDrawListener;

import androidx.annotation.UiThread;

import com.android.launcher3.view.ViewCaptureData.ExportedData;
import com.android.launcher3.view.ViewCaptureData.FrameData;
import com.android.launcher3.view.ViewCaptureData.ViewNode;

import java.util.concurrent.FutureTask;

/**
 * Utility class for capturing view data every frame
 */
public class ViewCapture implements OnDrawListener {

    private static final String TAG = "ViewCapture";

    private static final int MEMORY_SIZE = 2000;

    private final View mRoot;
    private final long[] mFrameTimes = new long[MEMORY_SIZE];
    private final Node[] mNodes = new Node[MEMORY_SIZE];

    private int mFrameIndex = -1;

    /**
     * @param root the root view for the capture data
     */
    public ViewCapture(View root) {
        mRoot = root;
    }

    @Override
    public void onDraw() {
        Trace.beginSection("view_capture");
        long now = SystemClock.elapsedRealtimeNanos();

        mFrameIndex++;
        if (mFrameIndex >= MEMORY_SIZE) {
            mFrameIndex = 0;
        }
        mFrameTimes[mFrameIndex] = now;
        mNodes[mFrameIndex] = captureView(mRoot, mNodes[mFrameIndex]);
        Trace.endSection();
    }

    /**
     * Creates a proto of all the data captured so far.
     */
    public String dumpToString() {
        Handler handler = mRoot.getHandler();
        if (handler == null) {
            handler = Executors.MAIN_EXECUTOR.getHandler();
        }
        FutureTask<ExportedData> task = new FutureTask<>(this::dumpToProtoUI);
        if (Looper.myLooper() == handler.getLooper()) {
            task.run();
        } else {
            handler.post(task);
        }
        try {
            return Base64.encodeToString(task.get().toByteArray(),
                    Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
        } catch (Exception e) {
            Log.e(TAG, "Error capturing proto", e);
            return "--error--";
        }
    }

    @UiThread
    private ExportedData dumpToProtoUI() {
        ExportedData.Builder dataBuilder = ExportedData.newBuilder();
        Resources res = mRoot.getResources();

        int size = (mNodes[MEMORY_SIZE - 1] == null) ? mFrameIndex + 1 : MEMORY_SIZE;
        for (int i = size - 1; i >= 0; i--) {
            int index = (MEMORY_SIZE + mFrameIndex - i) % MEMORY_SIZE;
            dataBuilder.addFrameData(FrameData.newBuilder()
                    .setNode(mNodes[index].toProto(res))
                    .setTimestamp(mFrameTimes[index]));
        }
        return dataBuilder.build();
    }

    private Node captureView(View view, Node recycle) {
        Node result = recycle == null ? new Node() : recycle;

        result.clazz = view.getClass();
        result.hashCode = view.hashCode();
        result.id = view.getId();
        result.left = view.getLeft();
        result.top = view.getTop();
        result.right = view.getRight();
        result.bottom = view.getBottom();
        result.scrollX = view.getScrollX();
        result.scrollY = view.getScrollY();

        result.translateX = view.getTranslationX();
        result.translateY = view.getTranslationY();
        result.scaleX = view.getScaleX();
        result.scaleY = view.getScaleY();
        result.alpha = view.getAlpha();

        result.visibility = view.getVisibility();
        result.willNotDraw = view.willNotDraw();

        if (view instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) view;
            result.clipChildren = parent.getClipChildren();
            int childCount = parent.getChildCount();
            if (childCount == 0) {
                result.children = null;
            } else {
                result.children = captureView(parent.getChildAt(0), result.children);
                Node lastChild = result.children;
                for (int i = 1; i < childCount; i++) {
                    lastChild.sibling = captureView(parent.getChildAt(i), lastChild.sibling);
                    lastChild = lastChild.sibling;
                }
                lastChild.sibling = null;
            }
        } else {
            result.clipChildren = false;
            result.children = null;
        }
        return result;
    }

    private static class Node {

        // We store reference in memory to avoid generating and storing too many strings
        public Class clazz;
        public int hashCode;

        public int id;
        public int left, top, right, bottom;
        public int scrollX, scrollY;

        public float translateX, translateY;
        public float scaleX, scaleY;
        public float alpha;

        public int visibility;
        public boolean willNotDraw;
        public boolean clipChildren;

        public Node sibling;
        public Node children;

        public ViewNode toProto(Resources res) {
            String resolvedId;
            if (id >= 0) {
                try {
                    resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id);
                } catch (Resources.NotFoundException e) {
                    resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
                }
            } else {
                resolvedId = "NO_ID";
            }

            ViewNode.Builder result = ViewNode.newBuilder()
                    .setClassname(clazz.getName() + "@" + hashCode)
                    .setId(resolvedId)
                    .setLeft(left)
                    .setTop(top)
                    .setWidth(right - left)
                    .setHeight(bottom - top)
                    .setTranslationX(translateX)
                    .setTranslationY(translateY)
                    .setScaleX(scaleX)
                    .setScaleY(scaleY)
                    .setAlpha(alpha)
                    .setVisibility(visibility)
                    .setWillNotDraw(willNotDraw)
                    .setClipChildren(clipChildren);
            Node child = children;
            while (child != null) {
                result.addChildren(child.toProto(res));
                child = child.sibling;
            }
            return result.build();
        }

    }
}