diff options
| -rw-r--r-- | api/current.txt | 2 | ||||
| -rw-r--r-- | api/system-current.txt | 2 | ||||
| -rw-r--r-- | core/java/android/app/Activity.java | 3 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 239 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 62 | ||||
| -rw-r--r-- | core/java/android/view/ViewRootImpl.java | 53 | ||||
| -rw-r--r-- | core/java/android/view/contentcapture/ChildContentCaptureSession.java | 5 | ||||
| -rw-r--r-- | core/java/android/view/contentcapture/ContentCaptureEvent.java | 35 | ||||
| -rw-r--r-- | core/java/android/view/contentcapture/ContentCaptureManager.java | 2 | ||||
| -rw-r--r-- | core/java/android/view/contentcapture/ContentCaptureSession.java | 9 | ||||
| -rw-r--r-- | core/java/android/view/contentcapture/MainContentCaptureSession.java | 51 | ||||
| -rw-r--r-- | core/java/com/android/internal/policy/DecorContext.java | 17 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java | 5 |
13 files changed, 419 insertions, 66 deletions
diff --git a/api/current.txt b/api/current.txt index ad321c6288b3..50ff701f1d52 100644 --- a/api/current.txt +++ b/api/current.txt @@ -50669,7 +50669,7 @@ package android.view { method public void setClickable(boolean); method public void setClipBounds(android.graphics.Rect); method public void setClipToOutline(boolean); - method public void setContentCaptureSession(@NonNull android.view.contentcapture.ContentCaptureSession); + method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession); method public void setContentDescription(CharSequence); method public void setContextClickable(boolean); method public void setDefaultFocusHighlightEnabled(boolean); diff --git a/api/system-current.txt b/api/system-current.txt index 4cb5eb0c343c..3942945e6dc3 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9299,6 +9299,8 @@ package android.view.contentcapture { method @Nullable public android.view.contentcapture.ViewNode getViewNode(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.view.contentcapture.ContentCaptureEvent> CREATOR; + field public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5; // 0x5 + field public static final int TYPE_INITIAL_VIEW_TREE_APPEARING = 4; // 0x4 field public static final int TYPE_VIEW_APPEARED = 1; // 0x1 field public static final int TYPE_VIEW_DISAPPEARED = 2; // 0x2 field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index aac8f0855ca9..0eadd1dcd903 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -7155,7 +7155,8 @@ public class Activity extends ContextThemeWrapper mInstrumentation.onEnterAnimationComplete(); onEnterAnimationComplete(); if (getWindow() != null && getWindow().getDecorView() != null) { - getWindow().getDecorView().getViewTreeObserver().dispatchOnEnterAnimationComplete(); + View decorView = getWindow().getDecorView(); + decorView.getViewTreeObserver().dispatchOnEnterAnimationComplete(); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index ab18fbdca85f..0295dc8d2de1 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -82,6 +82,7 @@ import android.os.Trace; import android.sysprop.DisplayProperties; import android.text.InputType; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.LayoutDirection; @@ -126,7 +127,6 @@ import android.widget.FrameLayout; import android.widget.ScrollBarDrawable; import com.android.internal.R; -import com.android.internal.util.Preconditions; import com.android.internal.view.TooltipPopup; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.widget.ScrollBarUtils; @@ -813,6 +813,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final String CONTENT_CAPTURE_LOG_TAG = "View.ContentCapture"; + private static final boolean DEBUG_CONTENT_CAPTURE = false; + /** * When set to true, this view will save its attribute data. * @@ -3393,9 +3395,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Masks for mPrivateFlags4, as generated by dumpFlags(): * * |-------|-------|-------|-------| - * 1111 PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK - * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED - * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED + * 1111 PFLAG4_IMPORTANT_FOR_CONTENT_CAPTURE_MASK + * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED + * 1 PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED + * 1 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED + * 1 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE + * 11 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK * |-------|-------|-------|-------| */ @@ -3422,6 +3427,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED = 0x10; private static final int PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED = 0x20; + /* + * Flags used to cache the value returned by isImportantForContentCapture while the view + * hierarchy is being traversed. + */ + private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED = 0x40; + private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE = 0x80; + + private static final int PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK = + PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED + | PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE; + /* End of masks for mPrivateFlags4 */ /** @hide */ @@ -5046,12 +5062,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * view (through {@link #setContentCaptureSession(ContentCaptureSession)}. */ @Nullable - private WeakReference<ContentCaptureSession> mContentCaptureSession; + private ContentCaptureSession mContentCaptureSession; @LayoutRes private int mSourceLayoutId = ID_NULL; /** + * Cached reference to the {@link ContentCaptureSession}, is reset on {@link #invalidate()}. + */ + private ContentCaptureSession mCachedContentCaptureSession; + + /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can @@ -8237,15 +8258,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </ul> */ public void onProvideContentCaptureStructure(@NonNull ViewStructure structure, int flags) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, - "onProvideContentCaptureStructure() for " + getClass().getSimpleName()); - } - try { - onProvideStructure(structure, VIEW_STRUCTURE_FOR_CONTENT_CAPTURE, flags); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } + onProvideStructure(structure, VIEW_STRUCTURE_FOR_CONTENT_CAPTURE, flags); } /** @hide */ @@ -8956,6 +8969,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS */ public final boolean isImportantForContentCapture() { + boolean isImportant; + if ((mPrivateFlags4 & PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED) != 0) { + isImportant = (mPrivateFlags4 & PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE) != 0; + return isImportant; + } + + isImportant = calculateIsImportantForContentCapture(); + + mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE; + if (isImportant) { + mPrivateFlags4 |= PFLAG4_CONTENT_CAPTURE_IMPORTANCE_CACHED_VALUE; + } + mPrivateFlags4 |= PFLAG4_CONTENT_CAPTURE_IMPORTANCE_IS_CACHED; + return isImportant; + } + + /** + * Calculates whether the flag is important for content capture so it can be used by + * {@link #isImportantForContentCapture()} while the tree is traversed. + */ + private boolean calculateIsImportantForContentCapture() { // Check parent mode to ensure we're important ViewParent parent = mParent; while (parent instanceof View) { @@ -8996,9 +9030,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } // View group is important if at least one children also is - //TODO(b/111276913): decide if we really need to send the relevant parents or just the - // leaves (with absolute coordinates). If it's the latter, then we need to update this - // javadoc and ViewGroup's implementation. if (this instanceof ViewGroup) { final ViewGroup group = (ViewGroup) this; for (int i = 0; i < group.getChildCount(); i++) { @@ -9035,6 +9066,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * </ol> */ private void notifyAppearedOrDisappearedForContentCaptureIfNeeded(boolean appeared) { + AttachInfo ai = mAttachInfo; + // Skip it while the view is being laided out for the first time + if (ai != null && !ai.mReadyForContentCaptureUpdates) return; + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCapture(" + appeared + ") for " + getClass().getSimpleName()); @@ -9047,24 +9082,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private void notifyAppearedOrDisappearedForContentCaptureIfNeededNoTrace(boolean appeared) { + AttachInfo ai = mAttachInfo; + // First check if context has client, so it saves a service lookup when it doesn't if (!mContext.isContentCaptureSupported()) return; // Then check if it's enabled in the context... - final ContentCaptureManager ccm = mContext.getSystemService(ContentCaptureManager.class); + final ContentCaptureManager ccm = ai != null ? ai.getContentCaptureManager(mContext) + : mContext.getSystemService(ContentCaptureManager.class); if (ccm == null || !ccm.isContentCaptureEnabled()) return; // ... and finally at the view level // NOTE: isImportantForContentCapture() is more expensive than cm.isContentCaptureEnabled() if (!isImportantForContentCapture()) return; - final ContentCaptureSession session = getContentCaptureSession(ccm); + ContentCaptureSession session = getContentCaptureSession(); if (session == null) return; if (appeared) { if (!isLaidOut() || getVisibility() != VISIBLE || (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED) != 0) { - if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) { + if (DEBUG_CONTENT_CAPTURE) { Log.v(CONTENT_CAPTURE_LOG_TAG, "Ignoring 'appeared' on " + this + ": laid=" + isLaidOut() + ", visibleToUser=" + isVisibleToUser() + ", visible=" + (getVisibility() == VISIBLE) @@ -9075,8 +9113,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } return; } - mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED; - mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED; + 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 @@ -9090,7 +9132,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { if ((mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED) == 0 || (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED) != 0) { - if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.VERBOSE)) { + if (DEBUG_CONTENT_CAPTURE) { Log.v(CONTENT_CAPTURE_LOG_TAG, "Ignoring 'disappeared' on " + this + ": laid=" + isLaidOut() + ", visibleToUser=" + isVisibleToUser() + ", visible=" + (getVisibility() == VISIBLE) @@ -9103,11 +9145,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED; mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED; - Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, - () -> session.notifyViewDisappeared(getAutofillId()), /* token= */ null); + + if (ai != null) { + ai.delayNotifyContentCaptureDisappeared(session, getAutofillId()); + } else { + if (DEBUG_CONTENT_CAPTURE) { + Log.v(CONTENT_CAPTURE_LOG_TAG, "no AttachInfo on gone for " + this); + } + Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, + () -> session.notifyViewDisappeared(getAutofillId()), /* token= */ null); + } } } + private void setNotifiedContentCaptureAppeared() { + mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED; + mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED; + } + /** * Sets the (optional) {@link ContentCaptureSession} associated with this view. * @@ -9131,9 +9186,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * {@link ContentCaptureSession#createContentCaptureSession( * android.view.contentcapture.ContentCaptureContext)}. */ - public void setContentCaptureSession(@NonNull ContentCaptureSession contentCaptureSession) { - mContentCaptureSession = new WeakReference<>( - Preconditions.checkNotNull(contentCaptureSession)); + public void setContentCaptureSession(@Nullable ContentCaptureSession contentCaptureSession) { + mContentCaptureSession = contentCaptureSession; } /** @@ -9145,8 +9199,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @Nullable public final ContentCaptureSession getContentCaptureSession() { + if (mCachedContentCaptureSession != null) { + return mCachedContentCaptureSession; + } + + mCachedContentCaptureSession = getAndCacheContentCaptureSession(); + return mCachedContentCaptureSession; + } + + @Nullable + private ContentCaptureSession getAndCacheContentCaptureSession() { // First try the session explicitly set by setContentCaptureSession() - if (mContentCaptureSession != null) return mContentCaptureSession.get(); + if (mContentCaptureSession != null) return mContentCaptureSession; // Then the session explicitly set in an ancestor ContentCaptureSession session = null; @@ -9163,21 +9227,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return session; } - /** - * Optimized version of {@link #getContentCaptureSession()} that avoids a service lookup. - */ - @Nullable - private ContentCaptureSession getContentCaptureSession(@NonNull ContentCaptureManager ccm) { - if (mContentCaptureSession != null) return mContentCaptureSession.get(); - - ContentCaptureSession session = null; - if (mParent instanceof View) { - session = ((View) mParent).getContentCaptureSession(ccm); - } - - return session != null ? session : ccm.getMainContentCaptureSession(); - } - @Nullable private AutofillManager getAutofillManager() { return mContext.getSystemService(AutofillManager.class); @@ -9350,6 +9399,62 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Dispatches the initial Content Capture events for a view structure. + * + * @hide + */ + public void dispatchInitialProvideContentCaptureStructure(@NonNull ContentCaptureManager ccm) { + AttachInfo ai = mAttachInfo; + if (ai == null) { + Log.w(CONTENT_CAPTURE_LOG_TAG, + "dispatchProvideContentCaptureStructure(): no AttachInfo 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 + // example, if important views are added) + ai.mReadyForContentCaptureUpdates = true; + + if (!isImportantForContentCapture()) { + if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.DEBUG)) { + Log.d(CONTENT_CAPTURE_LOG_TAG, + "dispatchProvideContentCaptureStructure(): decorView is not important"); + } + return; + } + + ai.mContentCaptureManager = ccm; + + ContentCaptureSession session = getContentCaptureSession(); + if (session == null) { + if (Log.isLoggable(CONTENT_CAPTURE_LOG_TAG, Log.DEBUG)) { + Log.d(CONTENT_CAPTURE_LOG_TAG, + "dispatchProvideContentCaptureStructure(): no session for " + this); + } + return; + } + + session.internalNotifyViewHierarchyEvent(/* started= */ true); + try { + dispatchProvideContentCaptureStructure(); + } finally { + session.internalNotifyViewHierarchyEvent(/* started= */ false); + } + } + + /** @hide */ + void dispatchProvideContentCaptureStructure() { + ContentCaptureSession session = getContentCaptureSession(); + if (session != null) { + ViewStructure structure = session.newViewStructure(this); + onProvideContentCaptureStructure(structure, /* flags= */ 0); + setNotifiedContentCaptureAppeared(); + session.notifyViewAppeared(structure); + } + } + + /** * @see #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) * * Note: Called from the default {@link AccessibilityDelegate}. @@ -17461,6 +17566,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return; } + // Reset content capture caches + mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK; + mCachedContentCaptureSession = null; + if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED @@ -27967,6 +28076,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, View mTooltipHost; /** + * The initial structure has been reported so the view is ready to report updates. + */ + boolean mReadyForContentCaptureUpdates; + + /** + * Map of ids (per session) that need to be notified after as gone the view hierchy is + * traversed. + */ + // TODO(b/121197119): use SparseArray once session id becomes integer + ArrayMap<String, ArrayList<AutofillId>> mContentCaptureRemovedIds; + + /** + * Cached reference to the {@link ContentCaptureManager}. + */ + ContentCaptureManager mContentCaptureManager; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * @@ -27984,6 +28110,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mRootCallbacks = effectPlayer; mTreeObserver = new ViewTreeObserver(context); } + + private void delayNotifyContentCaptureDisappeared(@NonNull ContentCaptureSession session, + @NonNull AutofillId id) { + if (mContentCaptureRemovedIds == null) { + // Most of the time there will be just one session, so intial capacity is 1 + mContentCaptureRemovedIds = 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); + } + ids.add(id); + } + + @Nullable + private ContentCaptureManager getContentCaptureManager(@NonNull Context context) { + if (mContentCaptureManager != null) { + return mContentCaptureManager; + } + mContentCaptureManager = context.getSystemService(ContentCaptureManager.class); + return mContentCaptureManager; + } } /** diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 0986cfa454b6..e0e784368bbb 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3603,7 +3603,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return; } - final ChildListForAutoFill children = getChildrenForAutofill(flags); + final ChildListForAutoFillOrContentCapture children = getChildrenForAutofill(flags); final int childrenCount = children.size(); structure.setChildCount(childrenCount); for (int i = 0; i < childrenCount; i++) { @@ -3614,13 +3614,31 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager children.recycle(); } + /** @hide */ + @Override + public void dispatchProvideContentCaptureStructure() { + super.dispatchProvideContentCaptureStructure(); + + if (!isLaidOut()) return; + + final ChildListForAutoFillOrContentCapture children = getChildrenForContentCapture(); + final int childrenCount = children.size(); + for (int i = 0; i < childrenCount; i++) { + final View child = children.get(i); + child.dispatchProvideContentCaptureStructure(); + } + children.recycle(); + } + /** * Gets the children for autofill. Children for autofill are the first * level descendants that are important for autofill. The returned * child list object is pooled and the caller must recycle it once done. * @hide */ - private @NonNull ChildListForAutoFill getChildrenForAutofill(@AutofillFlags int flags) { - final ChildListForAutoFill children = ChildListForAutoFill.obtain(); + private @NonNull ChildListForAutoFillOrContentCapture getChildrenForAutofill( + @AutofillFlags int flags) { + final ChildListForAutoFillOrContentCapture children = ChildListForAutoFillOrContentCapture + .obtain(); populateChildrenForAutofill(children, flags); return children; } @@ -3647,6 +3665,34 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + private @NonNull ChildListForAutoFillOrContentCapture getChildrenForContentCapture() { + final ChildListForAutoFillOrContentCapture children = ChildListForAutoFillOrContentCapture + .obtain(); + populateChildrenForContentCapture(children); + return children; + } + + /** @hide */ + private void populateChildrenForContentCapture(ArrayList<View> list) { + final int childrenCount = mChildrenCount; + if (childrenCount <= 0) { + return; + } + final ArrayList<View> preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + for (int i = 0; i < childrenCount; i++) { + final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); + final View child = (preorderedList == null) + ? mChildren[childIndex] : preorderedList.get(childIndex); + if (child.isImportantForContentCapture()) { + list.add(child); + } else if (child instanceof ViewGroup) { + ((ViewGroup) child).populateChildrenForContentCapture(list); + } + } + } + private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) { final View child; @@ -8544,16 +8590,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Pooled class that to hold the children for autifill. */ - static class ChildListForAutoFill extends ArrayList<View> { + private static class ChildListForAutoFillOrContentCapture extends ArrayList<View> { private static final int MAX_POOL_SIZE = 32; - private static final Pools.SimplePool<ChildListForAutoFill> sPool = + private static final Pools.SimplePool<ChildListForAutoFillOrContentCapture> sPool = new Pools.SimplePool<>(MAX_POOL_SIZE); - public static ChildListForAutoFill obtain() { - ChildListForAutoFill list = sPool.acquire(); + public static ChildListForAutoFillOrContentCapture obtain() { + ChildListForAutoFillOrContentCapture list = sPool.acquire(); if (list == null) { - list = new ChildListForAutoFill(); + list = new ChildListForAutoFillOrContentCapture(); } return list; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 61fb38f8298f..47528a05f5a2 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -103,7 +103,10 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; +import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; +import android.view.contentcapture.ContentCaptureManager; +import android.view.contentcapture.MainContentCaptureSession; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; @@ -154,6 +157,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_FPS = false; private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV; private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV; + private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -412,6 +416,7 @@ public final class ViewRootImpl implements ViewParent, boolean mApplyInsetsRequested; boolean mLayoutRequested; boolean mFirst; + boolean mPerformContentCapture; boolean mReportNextDraw; boolean mFullRedrawNeeded; boolean mNewSurfaceNeeded; @@ -608,6 +613,7 @@ public final class ViewRootImpl implements ViewParent, mTransparentRegion = new Region(); mPreviousTransparentRegion = new Region(); mFirst = true; // true for the first time the view is added + mPerformContentCapture = true; // also true for the first time the view is added mAdded = false; mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); @@ -2756,6 +2762,24 @@ public final class ViewRootImpl implements ViewParent, } } + if (mAttachInfo.mContentCaptureRemovedIds != null) { + 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 + .keyAt(i); + ArrayList<AutofillId> ids = mAttachInfo.mContentCaptureRemovedIds + .valueAt(i); + mainSession.notifyViewsDisappeared(sessionId, ids); + } + mAttachInfo.mContentCaptureRemovedIds = null; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + } + mIsInTraversal = false; } @@ -3451,6 +3475,35 @@ public final class ViewRootImpl implements ViewParent, pendingDrawFinished(); } } + if (mPerformContentCapture) { + performContentCapture(); + } + } + + private void performContentCapture() { + mPerformContentCapture = false; // One-time offer! + final View rootView = mView; + if (DEBUG_CONTENT_CAPTURE) { + Log.v(mTag, "dispatchContentCapture() 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.isContentCaptureSupported()) return; + + // Then check if it's enabled in the contex itself. + final ContentCaptureManager ccm = mContext + .getSystemService(ContentCaptureManager.class); + if (ccm == null || !ccm.isContentCaptureEnabled()) return; + + // Content capture is a go! + rootView.dispatchInitialProvideContentCaptureStructure(ccm); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } private boolean draw(boolean fullRedrawNeeded) { diff --git a/core/java/android/view/contentcapture/ChildContentCaptureSession.java b/core/java/android/view/contentcapture/ChildContentCaptureSession.java index 63c21f352e74..acb81e086461 100644 --- a/core/java/android/view/contentcapture/ChildContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ChildContentCaptureSession.java @@ -93,6 +93,11 @@ final class ChildContentCaptureSession extends ContentCaptureSession { } @Override + public void internalNotifyViewHierarchyEvent(boolean started) { + getMainCaptureSession().notifyInitialViewHierarchyEvent(mId, started); + } + + @Override boolean isContentCaptureEnabled() { return getMainCaptureSession().isContentCaptureEnabled(); } diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java index a6d44729aee5..91bae61d8d38 100644 --- a/core/java/android/view/contentcapture/ContentCaptureEvent.java +++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java @@ -69,13 +69,33 @@ public final class ContentCaptureEvent implements Parcelable { */ public static final int TYPE_VIEW_TEXT_CHANGED = 3; - // TODO(b/111276913): add event to indicate when FLAG_SECURE was changed? + /** + * Called before events (such as {@link #TYPE_VIEW_APPEARED}) representing the initial 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. + */ + 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. + * + * <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. + */ + public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5; /** @hide */ @IntDef(prefix = { "TYPE_" }, value = { TYPE_VIEW_APPEARED, TYPE_VIEW_DISAPPEARED, - TYPE_VIEW_TEXT_CHANGED + TYPE_VIEW_TEXT_CHANGED, + TYPE_INITIAL_VIEW_TREE_APPEARING, + TYPE_INITIAL_VIEW_TREE_APPEARED }) @Retention(RetentionPolicy.SOURCE) public @interface EventType{} @@ -108,8 +128,10 @@ public final class ContentCaptureEvent implements Parcelable { return this; } - private void setAutofillIds(@NonNull ArrayList<AutofillId> ids) { + /** @hide */ + public ContentCaptureEvent setAutofillIds(@NonNull ArrayList<AutofillId> ids) { mIds = Preconditions.checkNotNull(ids); + return this; } /** @@ -193,7 +215,8 @@ public final class ContentCaptureEvent implements Parcelable { * Gets the type of the event. * * @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED}, - * or {@link #TYPE_VIEW_TEXT_CHANGED}. + * {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_INITIAL_VIEW_TREE_APPEARING}, or + * {@link #TYPE_INITIAL_VIEW_TREE_APPEARED}. */ public @EventType int getType() { return mType; @@ -372,6 +395,10 @@ public final class ContentCaptureEvent implements Parcelable { return "VIEW_DISAPPEARED"; case TYPE_VIEW_TEXT_CHANGED: return "VIEW_TEXT_CHANGED"; + case TYPE_INITIAL_VIEW_TREE_APPEARING: + return "INITIAL_VIEW_HIERARCHY_STARTED"; + case TYPE_INITIAL_VIEW_TREE_APPEARED: + return "INITIAL_VIEW_HIERARCHY_FINISHED"; default: return "UKNOWN_TYPE: " + type; } diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index f31856c80477..8ae9e3c1fee7 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -46,7 +46,7 @@ import java.io.PrintWriter; * of every method. */ /** - * TODO(b/111276913): add javadocs / implement + * TODO(b/123577059): add javadocs / implement */ @SystemService(Context.CONTENT_CAPTURE_MANAGER_SERVICE) public final class ContentCaptureManager { diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java index 68a3e8a1eb32..e028961692f9 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -204,6 +204,12 @@ public abstract class ContentCaptureSession implements AutoCloseable { return mId.hashCode(); } + /** @hide */ + @NonNull + public String getId() { + return mId; + } + /** * Creates a new {@link ContentCaptureSession}. * @@ -362,6 +368,9 @@ public abstract class ContentCaptureSession implements AutoCloseable { abstract void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text); + /** @hide */ + public abstract void internalNotifyViewHierarchyEvent(boolean started); + /** * Creates a {@link ViewStructure} for a "standard" view. * diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 034c8fae0843..0605fa3f39a2 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -15,6 +15,8 @@ */ package android.view.contentcapture; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARING; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED; @@ -66,6 +68,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { private static final String TAG = MainContentCaptureSession.class.getSimpleName(); + private static final boolean FORCE_FLUSH = true; + /** * Handler message used to flush the buffer. */ @@ -270,6 +274,10 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } + private void handleSendEvent(@NonNull ContentCaptureEvent event) { + handleSendEvent(event, /* forceFlush= */ false); + } + private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) { final int eventType = event.getType(); if (VERBOSE) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event); @@ -518,6 +526,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } @Override + public void internalNotifyViewHierarchyEvent(boolean started) { + notifyInitialViewHierarchyEvent(mId, started); + } + + @Override boolean isContentCaptureEnabled() { return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled(); } @@ -536,33 +549,57 @@ public final class MainContentCaptureSession extends ContentCaptureSession { new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED) .setParentSessionId(parentSessionId) .setClientContext(clientContext), - /* forceFlush= */ true)); + FORCE_FLUSH)); } void notifyChildSessionFinished(@NonNull String parentSessionId, @NonNull String childSessionId) { mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED) - .setParentSessionId(parentSessionId), /* forceFlush= */ true)); + .setParentSessionId(parentSessionId), FORCE_FLUSH)); } void notifyViewAppeared(@NonNull String sessionId, @NonNull ViewStructureImpl node) { mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, - new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED) - .setViewNode(node.mNode), /* forceFlush= */ false)); + new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED).setViewNode(node.mNode))); } void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) { mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, - new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id), - /* forceFlush= */ false)); + new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id))); + } + + /** @hide */ + public void notifyViewsDisappeared(@NonNull String sessionId, + @NonNull ArrayList<AutofillId> ids) { + final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED); + if (ids.size() == 1) { + event.setAutofillId(ids.get(0)); + } else { + event.setAutofillIds(ids); + } + + mHandler.sendMessage( + obtainMessage(MainContentCaptureSession::handleSendEvent, this, event)); } void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id, @Nullable CharSequence text) { mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED).setAutofillId(id) - .setText(text), /* forceFlush= */ false)); + .setText(text))); + } + + void notifyInitialViewHierarchyEvent(@NonNull String sessionId, boolean started) { + if (started) { + mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, + new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARING))); + } else { + mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this, + new ContentCaptureEvent(sessionId, TYPE_INITIAL_VIEW_TREE_APPEARED), + FORCE_FLUSH)); + + } } @Override diff --git a/core/java/com/android/internal/policy/DecorContext.java b/core/java/com/android/internal/policy/DecorContext.java index cd80d53a7546..429c6187dfe0 100644 --- a/core/java/com/android/internal/policy/DecorContext.java +++ b/core/java/com/android/internal/policy/DecorContext.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.view.ContextThemeWrapper; import android.view.WindowManager; import android.view.WindowManagerImpl; +import android.view.contentcapture.ContentCaptureManager; import java.lang.ref.WeakReference; @@ -36,6 +37,7 @@ class DecorContext extends ContextThemeWrapper { private PhoneWindow mPhoneWindow; private WindowManager mWindowManager; private Resources mActivityResources; + private ContentCaptureManager mContentCaptureManager; private WeakReference<Context> mActivityContext; @@ -60,6 +62,16 @@ class DecorContext extends ContextThemeWrapper { } return mWindowManager; } + if (Context.CONTENT_CAPTURE_MANAGER_SERVICE.equals(name)) { + if (mContentCaptureManager == null) { + Context activityContext = mActivityContext.get(); + if (activityContext != null) { + mContentCaptureManager = (ContentCaptureManager) activityContext + .getSystemService(name); + } + } + return mContentCaptureManager; + } return super.getSystemService(name); } @@ -79,4 +91,9 @@ class DecorContext extends ContextThemeWrapper { public AssetManager getAssets() { return mActivityResources.getAssets(); } + + @Override + public boolean isContentCaptureSupported() { + return true; + } } diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java index 71612e685920..34fdebfdf348 100644 --- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java @@ -155,5 +155,10 @@ public class ContentCaptureSessionTest { void internalNotifyViewTextChanged(AutofillId id, CharSequence text) { throw new UnsupportedOperationException("should not have been called"); } + + @Override + public void internalNotifyViewHierarchyEvent(boolean started) { + throw new UnsupportedOperationException("should not have been called"); + } } } |