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

Commit 4a79c637 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topic "view_tree_batch"

* changes:
  Changed Content Catpure workflow so it notifies when multiple changes are made.
  Improved how Content Capture events are flushed when activity is resumed / paused.
parents 3d61f892 544b39cb
Loading
Loading
Loading
Loading
+1 −18
Original line number Diff line number Diff line
@@ -127,7 +127,6 @@ import android.view.autofill.AutofillPopupWindow;
import android.view.autofill.IAutofillWindowPresenter;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.widget.AdapterView;
import android.widget.Toast;
import android.widget.Toolbar;
@@ -1038,15 +1037,11 @@ public class Activity extends ContextThemeWrapper
    }

    /** @hide */ private static final int CONTENT_CAPTURE_START = 1;
    /** @hide */ private static final int CONTENT_CAPTURE_PAUSE = 2;
    /** @hide */ private static final int CONTENT_CAPTURE_RESUME = 3;
    /** @hide */ private static final int CONTENT_CAPTURE_STOP = 4;
    /** @hide */ private static final int CONTENT_CAPTURE_STOP = 2;

    /** @hide */
    @IntDef(prefix = { "CONTENT_CAPTURE_" }, value = {
            CONTENT_CAPTURE_START,
            CONTENT_CAPTURE_PAUSE,
            CONTENT_CAPTURE_RESUME,
            CONTENT_CAPTURE_STOP
    })
    @Retention(RetentionPolicy.SOURCE)
@@ -1056,10 +1051,6 @@ public class Activity extends ContextThemeWrapper
        switch (type) {
            case CONTENT_CAPTURE_START:
                return "START";
            case CONTENT_CAPTURE_PAUSE:
                return "PAUSE";
            case CONTENT_CAPTURE_RESUME:
                return "RESUME";
            case CONTENT_CAPTURE_STOP:
                return "STOP";
            default:
@@ -1088,12 +1079,6 @@ public class Activity extends ContextThemeWrapper
                    }
                    cm.onActivityStarted(mToken, getComponentName(), flags);
                    break;
                case CONTENT_CAPTURE_PAUSE:
                    cm.flush(ContentCaptureSession.FLUSH_REASON_ACTIVITY_PAUSED);
                    break;
                case CONTENT_CAPTURE_RESUME:
                    cm.flush(ContentCaptureSession.FLUSH_REASON_ACTIVITY_RESUMED);
                    break;
                case CONTENT_CAPTURE_STOP:
                    cm.onActivityStopped();
                    break;
@@ -1781,7 +1766,6 @@ public class Activity extends ContextThemeWrapper
            }
        }
        mCalled = true;
        notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_RESUME);
    }

    /**
@@ -2198,7 +2182,6 @@ public class Activity extends ContextThemeWrapper
            }
        }
        mCalled = true;
        notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_PAUSE);
    }

    /**
+30 −32
Original line number Diff line number Diff line
@@ -9408,20 +9408,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            }
            setNotifiedContentCaptureAppeared();
            // TODO(b/123307965): instead of post, we should queue it on AttachInfo and then
            // dispatch on RootImpl, as we're doing with the removed ones (in that case, we should
            // merge the delayNotifyContentCaptureDisappeared() into a more generic method that
            // takes a session and a command, where the command is either view added or removed
            // The code below doesn't take much for a unique view, but it's called for all views
            // the first time the view hiearchy is laid off, which could acccumulative delay the
            // initial layout. Hence, we're postponing it to a later stage - it might still cost a
            // lost frame (or more), but that jank cost would only happen after the 1st layout.
            Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> {
                final ViewStructure structure = session.newViewStructure(this);
                onProvideContentCaptureStructure(structure, /* flags= */ 0);
                session.notifyViewAppeared(structure);
            }, /* token= */ null);
            if (ai != null) {
                ai.delayNotifyContentCaptureEvent(session, this, appeared);
            } else {
                if (DEBUG_CONTENT_CAPTURE) {
                    Log.w(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on appeared for " + this);
                }
            }
        } else {
            if ((mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED) == 0
                    || (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED) != 0) {
@@ -9440,13 +9433,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED;
            if (ai != null) {
                ai.delayNotifyContentCaptureDisappeared(session, getAutofillId());
                ai.delayNotifyContentCaptureEvent(session, this, appeared);
            } else {
                if (DEBUG_CONTENT_CAPTURE) {
                    Log.v(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on gone for " + this);
                    Log.v(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on disappeared for " + this);
                }
                Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT,
                        () -> session.notifyViewDisappeared(getAutofillId()), /* token= */ null);
            }
        }
    }
@@ -9703,13 +9694,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     *
     * @hide
     */
    public void dispatchInitialProvideContentCaptureStructure(@NonNull ContentCaptureManager ccm) {
    public void dispatchInitialProvideContentCaptureStructure() {
        AttachInfo ai = mAttachInfo;
        if (ai == null) {
            Log.w(CONTENT_CAPTURE_LOG_TAG,
                    "dispatchProvideContentCaptureStructure(): no AttachInfo for " + this);
            return;
        }
        ContentCaptureManager ccm = ai.mContentCaptureManager;
        if (ccm == null) {
            Log.w(CONTENT_CAPTURE_LOG_TAG, "dispatchProvideContentCaptureStructure(): "
                    + "no ContentCaptureManager for " + this);
            return;
        }
        // We must set it before checkign if the view itself is important, because it might
        // initially not be (for example, if it's empty), although that might change later (for
@@ -28358,11 +28355,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        boolean mReadyForContentCaptureUpdates;
        /**
         * Map of ids (per session) that need to be notified after as gone the view hierchy is
         * traversed.
         * Map(keyed by session) of content capture events that need to be notified after the view
         * hierarchy is traversed: value is either the view itself for appearead events, or its
         * autofill id for disappeared.
         */
        // TODO(b/121197119): use SparseArray once session id becomes integer
        ArrayMap<String, ArrayList<AutofillId>> mContentCaptureRemovedIds;
        ArrayMap<String, ArrayList<Object>> mContentCaptureEvents;
        /**
         * Cached reference to the {@link ContentCaptureManager}.
@@ -28388,24 +28386,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            mTreeObserver = new ViewTreeObserver(context);
        }
        private void delayNotifyContentCaptureDisappeared(@NonNull ContentCaptureSession session,
                @NonNull AutofillId id) {
            if (mContentCaptureRemovedIds == null) {
        private void delayNotifyContentCaptureEvent(@NonNull ContentCaptureSession session,
                @NonNull View view, boolean appeared) {
            if (mContentCaptureEvents == null) {
                // Most of the time there will be just one session, so intial capacity is 1
                mContentCaptureRemovedIds = new ArrayMap<>(1);
                mContentCaptureEvents = new ArrayMap<>(1);
            }
            String sessionId = session.getId();
            // TODO: life would be much easier if we provided a MultiMap implementation somwhere...
            ArrayList<AutofillId> ids = mContentCaptureRemovedIds.get(sessionId);
            if (ids == null) {
                ids = new ArrayList<>();
                mContentCaptureRemovedIds.put(sessionId, ids);
            ArrayList<Object> events = mContentCaptureEvents.get(sessionId);
            if (events == null) {
                events = new ArrayList<>();
                mContentCaptureEvents.put(sessionId, events);
            }
            ids.add(id);
            events.add(appeared ? view : view.getAutofillId());
        }
        @Nullable
        private ContentCaptureManager getContentCaptureManager(@NonNull Context context) {
        ContentCaptureManager getContentCaptureManager(@NonNull Context context) {
            if (mContentCaptureManager != null) {
                return mContentCaptureManager;
            }
+141 −27
Original line number Diff line number Diff line
@@ -108,6 +108,7 @@ import android.view.animation.Interpolator;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.MainContentCaptureSession;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
@@ -223,10 +224,25 @@ public final class ViewRootImpl implements ViewParent,
     */
    static final int MAX_TRACKBALL_DELAY = 250;

    /**
     * Initial value for {@link #mContentCaptureEnabled}.
     */
    private static final int CONTENT_CAPTURE_ENABLED_NOT_CHECKED = 0;

    /**
     * Value for {@link #mContentCaptureEnabled} when it was checked and set to {@code true}.
     */
    private static final int CONTENT_CAPTURE_ENABLED_TRUE = 1;

    /**
     * Value for {@link #mContentCaptureEnabled} when it was checked and set to {@code false}.
     */
    private static final int CONTENT_CAPTURE_ENABLED_FALSE = 2;

    @UnsupportedAppUsage
    static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();

    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList();
    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
    static boolean sFirstDrawComplete = false;

    /**
@@ -418,7 +434,11 @@ public final class ViewRootImpl implements ViewParent,
    boolean mApplyInsetsRequested;
    boolean mLayoutRequested;
    boolean mFirst;

    @Nullable
    int mContentCaptureEnabled = CONTENT_CAPTURE_ENABLED_NOT_CHECKED;
    boolean mPerformContentCapture;

    boolean mReportNextDraw;
    boolean mFullRedrawNeeded;
    boolean mNewSurfaceNeeded;
@@ -2776,27 +2796,60 @@ public final class ViewRootImpl implements ViewParent,
            }
        }

        if (mAttachInfo.mContentCaptureRemovedIds != null) {
        if (mAttachInfo.mContentCaptureEvents != null) {
            notifyContentCatpureEvents();
        }

        mIsInTraversal = false;
    }

    private void notifyContentCatpureEvents() {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
        try {
            MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
                    .getMainContentCaptureSession();
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureViewsGone");
            try {
                for (int i = 0; i < mAttachInfo.mContentCaptureRemovedIds.size(); i++) {
                    String sessionId = mAttachInfo.mContentCaptureRemovedIds
            if (mainSession == null) {
                Log.w(mTag, "no MainContentCaptureSession on AttachInfo");
                return;
            }
            for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) {
                String sessionId = mAttachInfo.mContentCaptureEvents
                        .keyAt(i);
                    ArrayList<AutofillId> ids = mAttachInfo.mContentCaptureRemovedIds
                mainSession.notifyViewHierarchyEvent(sessionId, /* started = */ true);
                ArrayList<Object> events = mAttachInfo.mContentCaptureEvents
                        .valueAt(i);
                    mainSession.notifyViewsDisappeared(sessionId, ids);
                for_each_event: for (int j = 0; j < events.size(); j++) {
                    Object event = events.get(j);
                    if (event instanceof AutofillId) {
                        mainSession.notifyViewDisappeared(sessionId, (AutofillId) event);
                    } else if (event instanceof View) {
                        View view = (View) event;
                        ContentCaptureSession session = view.getContentCaptureSession();
                        if (session == null) {
                            Log.w(mTag, "no content capture session on view: " + view);
                            continue for_each_event;
                        }
                        String actualId = session.getId().toString();
                        if (!actualId.equals(sessionId)) {
                            Log.w(mTag, "content capture session mismatch for view (" + view
                                    + "): was " + sessionId + " before, it's " + actualId + " now");
                            continue for_each_event;
                        }
                        ViewStructure structure = session.newViewStructure(view);
                        view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
                        session.notifyViewAppeared(structure);
                    } else {
                        Log.w(mTag, "invalid content capture event: " + event);
                    }
                mAttachInfo.mContentCaptureRemovedIds = null;
                }
                mainSession.notifyViewHierarchyEvent(sessionId, /* started = */ false);
            }
            mAttachInfo.mContentCaptureEvents = null;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

        mIsInTraversal = false;
    }

    private void notifySurfaceDestroyed() {
        mSurfaceHolder.ungetCallbacks();
        SurfaceHolder.Callback[] callbacks = mSurfaceHolder.getCallbacks();
@@ -2927,6 +2980,13 @@ public final class ViewRootImpl implements ViewParent,
            }
        }
        mFirstInputStage.onWindowFocusChanged(hasWindowFocus);
        // NOTE: there's no view visibility (appeared / disapparead) events when the windows focus
        // is lost, so we don't need to to force a flush - there might be other events such as
        // text changes, but these should be flushed independently.
        if (hasWindowFocus) {
            performContentCaptureFlushIfNecessary(
                    ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED);
        }
    }

    private void fireAccessibilityFocusEventIfHasFocusedNode() {
@@ -3494,36 +3554,90 @@ public final class ViewRootImpl implements ViewParent,
            }
        }
        if (mPerformContentCapture) {
            performContentCapture();
            performContentCaptureInitialReport();
        }
    }

    private void performContentCapture() {
    /**
     * Checks (and caches) if content capture is enabled for this context.
     */
    private boolean isContentCaptureEnabled() {
        switch (mContentCaptureEnabled) {
            case CONTENT_CAPTURE_ENABLED_TRUE:
                return true;
            case CONTENT_CAPTURE_ENABLED_FALSE:
                return false;
            case CONTENT_CAPTURE_ENABLED_NOT_CHECKED:
                final boolean reallyEnabled = isContentCaptureReallyEnabled();
                mContentCaptureEnabled = reallyEnabled ? CONTENT_CAPTURE_ENABLED_TRUE
                        : CONTENT_CAPTURE_ENABLED_FALSE;
                return reallyEnabled;
            default:
                Log.w(TAG, "isContentCaptureEnabled(): invalid state " + mContentCaptureEnabled);
                return false;
        }

    }

    /**
     * Checks (without caching) if content capture is enabled for this context.
     */
    private boolean isContentCaptureReallyEnabled() {
        // First check if context supports it, so it saves a service lookup when it doesn't
        if (mContext.getContentCaptureOptions() == null) return false;

        final ContentCaptureManager ccm = mAttachInfo.getContentCaptureManager(mContext);
        // Then check if it's enabled in the contex itself.
        if (ccm == null || !ccm.isContentCaptureEnabled()) return false;

        return true;
    }

    private void performContentCaptureInitialReport() {
        mPerformContentCapture = false; // One-time offer!
        final View rootView = mView;
        if (DEBUG_CONTENT_CAPTURE) {
            Log.v(mTag, "dispatchContentCapture() on " + rootView);
            Log.v(mTag, "performContentCaptureInitialReport() on " + rootView);
        }
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchContentCapture() for "
                    + getClass().getSimpleName());
        }
        try {
            // First check if context supports it, so it saves a service lookup when it doesn't
            if (mContext.getContentCaptureOptions() == null) return;

            // Then check if it's enabled in the contex itself.
            final ContentCaptureManager ccm = mContext
                    .getSystemService(ContentCaptureManager.class);
            if (ccm == null || !ccm.isContentCaptureEnabled()) return;
            if (!isContentCaptureEnabled()) return;

            // Content capture is a go!
            rootView.dispatchInitialProvideContentCaptureStructure(ccm);
            rootView.dispatchInitialProvideContentCaptureStructure();
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

    private void performContentCaptureFlushIfNecessary(
            @ContentCaptureSession.FlushReason int flushReason) {
        if (DEBUG_CONTENT_CAPTURE) {
            Log.v(mTag, "performContentCaptureFlushIfNecessary("
                    + ContentCaptureSession.getFlushReasonAsString(flushReason) + ")");
        }
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "flushContentCapture for "
                    + getClass().getSimpleName());
        }
        try {
            if (!isContentCaptureEnabled()) return;

            final ContentCaptureManager ccm = mAttachInfo.mContentCaptureManager;
            if (ccm == null) {
                Log.w(TAG, "flush content capture: no ContentCapture on AttachInfo");
                return;
            }
            ccm.flush(flushReason);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }


    private boolean draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
        if (!surface.isValid()) {
+1 −1
Original line number Diff line number Diff line
@@ -85,7 +85,7 @@ final class ChildContentCaptureSession extends ContentCaptureSession {

    @Override
    public void internalNotifyViewHierarchyEvent(boolean started) {
        getMainCaptureSession().notifyInitialViewHierarchyEvent(mId, started);
        getMainCaptureSession().notifyViewHierarchyEvent(mId, started);
    }

    @Override
+8 −6
Original line number Diff line number Diff line
@@ -72,23 +72,25 @@ public final class ContentCaptureEvent implements Parcelable {
    public static final int TYPE_VIEW_TEXT_CHANGED = 3;

    /**
     * Called before events (such as {@link #TYPE_VIEW_APPEARED}) representing the initial view
     * hierarchy are sent.
     * Called before events (such as {@link #TYPE_VIEW_APPEARED} and/or
     * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy are sent.
     *
     * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
     * if the initial view hierarchy doesn't initially have any view that's important for content
     * capture.
     */
    // TODO(b/125395044): change to TYPE_VIEW_TREE_APPEARING
    public static final int TYPE_INITIAL_VIEW_TREE_APPEARING = 4;

    /**
     * Called after events (such as {@link #TYPE_VIEW_APPEARED}) representing the initial view
     * hierarchy are sent.
     * Called after events (such as {@link #TYPE_VIEW_APPEARED} and/or
     * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy were sent.
     *
     * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
     * if the initial view hierarchy doesn't initially have any view that's important for content
     * capture.
     */
    // TODO(b/125395044): change to TYPE_VIEW_TREE_APPEARED
    public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5;

    /**
@@ -418,9 +420,9 @@ public final class ContentCaptureEvent implements Parcelable {
            case TYPE_VIEW_TEXT_CHANGED:
                return "VIEW_TEXT_CHANGED";
            case TYPE_INITIAL_VIEW_TREE_APPEARING:
                return "INITIAL_VIEW_HIERARCHY_STARTED";
                return "VIEW_TREE_APPEARING";
            case TYPE_INITIAL_VIEW_TREE_APPEARED:
                return "INITIAL_VIEW_HIERARCHY_FINISHED";
                return "VIEW_TREE_APPEARED";
            case TYPE_CONTEXT_UPDATED:
                return "CONTEXT_UPDATED";
            default:
Loading