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

Commit 7c3c32d9 authored by Kean Mariotti's avatar Kean Mariotti Committed by Android (Google) Code Review
Browse files

Merge "viewcapture: support concurrent UI threads" into main

parents 6bbf8059 8f546b07
Loading
Loading
Loading
Loading
+100 −53
Original line number Diff line number Diff line
@@ -24,11 +24,13 @@ import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.media.permission.SafeCloseable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import android.os.Trace;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@@ -52,6 +54,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -77,6 +80,10 @@ public abstract class ViewCapture {

    // Number of frames to keep in memory
    private final int mMemorySize;

    // Number of ViewPropertyRef to preallocate per window
    private final int mInitPoolSize;

    protected static final int DEFAULT_MEMORY_SIZE = 2000;
    // Initial size of the reference pool. This is at least be 5 * total number of views in
    // Launcher. This allows the first free frames avoid object allocation during view capture.
@@ -84,18 +91,16 @@ public abstract class ViewCapture {

    public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());

    private final List<WindowListener> mListeners = new ArrayList<>();
    private final List<WindowListener> mListeners = Collections.synchronizedList(new ArrayList<>());

    protected final Executor mBgExecutor;

    // Pool used for capturing view tree on the UI thread.
    private ViewPropertyRef mPool = new ViewPropertyRef();
    private boolean mIsEnabled = true;

    protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) {
        mMemorySize = memorySize;
        mBgExecutor = bgExecutor;
        mBgExecutor.execute(() -> initPool(initPoolSize));
        mInitPoolSize = initPoolSize;
    }

    public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
@@ -104,29 +109,10 @@ public abstract class ViewCapture {
        return new LooperExecutor(thread.getLooper());
    }

    @UiThread
    private void addToPool(ViewPropertyRef start, ViewPropertyRef end) {
        end.next = mPool;
        mPool = start;
    }

    @WorkerThread
    private void initPool(int initPoolSize) {
        ViewPropertyRef start = new ViewPropertyRef();
        ViewPropertyRef current = start;

        for (int i = 0; i < initPoolSize; i++) {
            current.next = new ViewPropertyRef();
            current = current.next;
        }

        ViewPropertyRef finalCurrent = current;
        MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent));
    }

    /**
     * Attaches the ViewCapture to the provided window and returns a handle to detach the listener
     */
    @AnyThread
    @NonNull
    public SafeCloseable startCapture(@NonNull Window window) {
        String title = window.getAttributes().getTitle().toString();
@@ -138,18 +124,25 @@ public abstract class ViewCapture {
     * Attaches the ViewCapture to the provided window and returns a handle to detach the listener.
     * Verifies that ViewCapture is enabled before actually attaching an onDrawListener.
     */
    @AnyThread
    @NonNull
    public SafeCloseable startCapture(@NonNull View view, @NonNull String name) {
        WindowListener listener = new WindowListener(view, name);
        if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot);

        if (mIsEnabled) {
            listener.attachToRoot();
        }

        mListeners.add(listener);
        view.getContext().registerComponentCallbacks(listener);

        runOnUiThread(() -> view.getContext().registerComponentCallbacks(listener), view);

        return () -> {
            if (listener.mRoot != null && listener.mRoot.getContext() != null) {
                listener.mRoot.getContext().unregisterComponentCallbacks(listener);
            }
            mListeners.remove(listener);

            listener.detachFromRoot();
        };
    }
@@ -164,16 +157,18 @@ public abstract class ViewCapture {
     * are still technically enabled to allow for dumping.
     */
    @VisibleForTesting
    @AnyThread
    public void stopCapture(@NonNull View rootView) {
        mListeners.forEach(it -> {
            if (rootView == it.mRoot) {
                it.mRoot.getViewTreeObserver().removeOnDrawListener(it);
                runOnUiThread(() -> it.mRoot.getViewTreeObserver().removeOnDrawListener(it),
                        it.mRoot);
                it.mRoot = null;
            }
        });
    }

    @UiThread
    @AnyThread
    protected void enableOrDisableWindowListeners(boolean isEnabled) {
        mIsEnabled = isEnabled;
        mListeners.forEach(WindowListener::detachFromRoot);
@@ -234,6 +229,24 @@ public abstract class ViewCapture {
            ViewPropertyRef startFlattenedViewTree) {
    }

    @AnyThread
    void runOnUiThread(Runnable action, View view) {
        if (view == null) {
            // Corner case. E.g.: the capture is stopped (root view set to null),
            // but the bg thread is still processing work.
            Log.i(TAG, "Skipping run on UI thread. Provided view == null.");
            return;
        }

        Handler handlerUi = view.getHandler();
        if (handlerUi != null && handlerUi.getLooper().getThread() == Thread.currentThread()) {
            action.run();
            return;
        }

        view.post(action);
    }

    /**
     * Once this window listener is attached to a window's root view, it traverses the entire
     * view tree on the main thread every time onDraw is called. It then saves the state of the view
@@ -283,6 +296,8 @@ public abstract class ViewCapture {
        public View mRoot;
        public final String name;

        // Pool used for capturing view tree on the UI thread.
        private ViewPropertyRef mPool = new ViewPropertyRef();
        private final ViewPropertyRef mViewPropertyRef = new ViewPropertyRef();

        private int mFrameIndexBg = -1;
@@ -297,6 +312,7 @@ public abstract class ViewCapture {
        WindowListener(View view, String name) {
            mRoot = view;
            this.name = name;
            initPool(mInitPoolSize);
        }

        /**
@@ -306,6 +322,7 @@ public abstract class ViewCapture {
         * thread via mExecutor.
         */
        @Override
        @UiThread
        public void onDraw() {
            Trace.beginSection("vc#onDraw");
            captureViewTree(mRoot, mViewPropertyRef);
@@ -397,7 +414,7 @@ public abstract class ViewCapture {
                    // The compiler will complain about using a non-final variable from
                    // an outer class in a lambda if we pass in 'end' directly.
                    final ViewPropertyRef finalEnd = end;
                    MAIN_EXECUTOR.execute(() -> addToPool(start, finalEnd));
                    runOnUiThread(() -> addToPool(start, finalEnd), mRoot);
                    break;
                }
                end = end.next;
@@ -409,6 +426,7 @@ public abstract class ViewCapture {
            Trace.endSection();
        }

        @WorkerThread
        private @Nullable ViewPropertyRef findInLastFrame(int hashCode) {
            int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1;
            ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex];
@@ -418,9 +436,41 @@ public abstract class ViewCapture {
            return viewPropertyRef;
        }

        private void initPool(int initPoolSize) {
            ViewPropertyRef start = new ViewPropertyRef();
            ViewPropertyRef current = start;

            for (int i = 0; i < initPoolSize; i++) {
                current.next = new ViewPropertyRef();
                current = current.next;
            }

            ViewPropertyRef finalCurrent = current;
            addToPool(start, finalCurrent);
        }

        private void addToPool(ViewPropertyRef start, ViewPropertyRef end) {
            end.next = mPool;
            mPool = start;
        }

        @UiThread
        private ViewPropertyRef getFromPool() {
            ViewPropertyRef ref = mPool;
            if (ref != null) {
                mPool = ref.next;
                ref.next = null;
            } else {
                ref = new ViewPropertyRef();
            }
            return ref;
        }

        @AnyThread
        void attachToRoot() {
            if (mRoot == null) return;
            mIsActive = true;
            runOnUiThread(() -> {
                if (mRoot.isAttachedToWindow()) {
                    safelyEnableOnDrawListener();
                } else {
@@ -438,15 +488,18 @@ public abstract class ViewCapture {
                        }
                    });
                }
            }, mRoot);
        }

        @AnyThread
        void detachFromRoot() {
            mIsActive = false;
            if (mRoot != null) {
                mRoot.getViewTreeObserver().removeOnDrawListener(this);
                runOnUiThread(() -> mRoot.getViewTreeObserver().removeOnDrawListener(this), mRoot);
            }
        }

        @UiThread
        private void safelyEnableOnDrawListener() {
            if (mRoot != null) {
                mRoot.getViewTreeObserver().removeOnDrawListener(this);
@@ -470,15 +523,9 @@ public abstract class ViewCapture {
            return builder.build();
        }

        @UiThread
        private ViewPropertyRef captureViewTree(View view, ViewPropertyRef start) {
            ViewPropertyRef ref;
            if (mPool != null) {
                ref = mPool;
                mPool = mPool.next;
                ref.next = null;
            } else {
                ref = new ViewPropertyRef();
            }
            ViewPropertyRef ref = getFromPool();
            start.next = ref;
            if (view instanceof ViewGroup) {
                ViewGroup parent = (ViewGroup) view;