Adding support for continously capturing view hierarcy in Launcher

Bug: 238243939
Test: Verified data being captured and dumped
Change-Id: Ibe069d39ccf728f7b953f85085e58976be6e05ac
diff --git a/protos/view_capture.proto b/protos/view_capture.proto
new file mode 100644
index 0000000..98574dd
--- /dev/null
+++ b/protos/view_capture.proto
@@ -0,0 +1,54 @@
+/*
+ * 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;
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5081f4f..af088af 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -194,6 +194,7 @@
 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 @@
     private LauncherState mPrevLauncherState;
 
     private StringCache mStringCache;
+    private ViewCapture mViewCapture;
 
     @Override
     @TargetApi(Build.VERSION_CODES.S)
@@ -1478,6 +1480,14 @@
     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 @@
         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);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index b7f3dad..4fd13b2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -285,6 +285,9 @@
             "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) {
diff --git a/src/com/android/launcher3/util/ViewCapture.java b/src/com/android/launcher3/util/ViewCapture.java
new file mode 100644
index 0000000..140971b
--- /dev/null
+++ b/src/com/android/launcher3/util/ViewCapture.java
@@ -0,0 +1,212 @@
+/*
+ * 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();
+        }
+
+    }
+}