summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt2
-rw-r--r--api/system-current.txt2
-rw-r--r--core/java/android/app/Activity.java3
-rw-r--r--core/java/android/view/View.java239
-rw-r--r--core/java/android/view/ViewGroup.java62
-rw-r--r--core/java/android/view/ViewRootImpl.java53
-rw-r--r--core/java/android/view/contentcapture/ChildContentCaptureSession.java5
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureEvent.java35
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureManager.java2
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureSession.java9
-rw-r--r--core/java/android/view/contentcapture/MainContentCaptureSession.java51
-rw-r--r--core/java/com/android/internal/policy/DecorContext.java17
-rw-r--r--core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java5
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");
+ }
}
}