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

Commit 8f546b07 authored by Kean Mariotti's avatar Kean Mariotti
Browse files

viewcapture: support concurrent UI threads

ViewCapture was originally designed for apps with a single UI thread and
WindowListener#onDraw() (as well as some other less frequent methods)
was meant to be executed only by the process' main thread.

This commit relaxes the "main thread only" assumption and allows multiple
UI threads to execute WindowListener#onDraw() concurrently. This change
is needed to onboard new apps with multiple UI threads (e.g. sysui).

Bug: 375005884
Flag: EXEMPT bugfix
Test: enable viewcapture on sysui windows and check the NPE \
        doesn't happen anymore on presubmit
Change-Id: I8c55553b3ea69f4cc772df1eae3a26f8d22bf3ce
parent eb30074f
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;