diff options
5 files changed, 184 insertions, 1 deletions
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 9cd23250d673..2483f99a07ec 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -630,7 +630,7 @@ public final class DeviceConfig { private static final List<String> PUBLIC_NAMESPACES = Arrays.asList(NAMESPACE_TEXTCLASSIFIER, NAMESPACE_RUNTIME, NAMESPACE_STATSD_JAVA, NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL, - NAMESPACE_DEVICE_POLICY_MANAGER); + NAMESPACE_DEVICE_POLICY_MANAGER, NAMESPACE_CONTENT_CAPTURE); /** * Privacy related properties definitions. * diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java index e2466345ff4a..2c2ae06e9186 100644 --- a/core/java/android/view/ViewStructure.java +++ b/core/java/android/view/ViewStructure.java @@ -45,6 +45,30 @@ import java.util.List; public abstract class ViewStructure { /** + * Key used for writing active child view information to the content capture bundle. + * + * The value stored under this key will be an ordered list of Autofill IDs of child views. + * + * TODO(b/241498401): Add @TestApi in Android U + * @hide + */ + public static final String EXTRA_ACTIVE_CHILDREN_IDS = + "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS"; + + /** + * Key used for writing the first active child's position to the content capture bundle. + * + * When active child view information is provided under the + * {@link #EXTRA_ACTIVE_CHILDREN_IDS}, the value stored under this key will be the + * 0-based position of the first child view in the list relative to the positions of child views + * in the containing View's dataset. + * + * TODO(b/241498401): Add @TestApi in Android U + * @hide */ + public static final String EXTRA_FIRST_ACTIVE_POSITION = + "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION"; + + /** * Set the identifier for this view. * * @param id The view's identifier, as per {@link View#getId View.getId()}. diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java index ba4176faa283..db4ac5de0b49 100644 --- a/core/java/android/view/contentcapture/ContentCaptureEvent.java +++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java @@ -55,6 +55,15 @@ public final class ContentCaptureEvent implements Parcelable { /** * Called when a node has been added to the screen and is visible to the user. * + * On API level 33, this event may be re-sent with additional information if a view's children + * have changed, e.g. scrolling Views inside of a ListView. This information will be stored in + * the extras Bundle associated with the event's ViewNode. Within the Bundle, the + * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" key may be used to get a list of + * Autofill IDs of active child views, and the + * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" key may be used to get the 0-based + * position of the first active child view in the list relative to the positions of child views + * in the container View's dataset. + * * <p>The metadata of the node is available through {@link #getViewNode()}. */ public static final int TYPE_VIEW_APPEARED = 1; diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 48d29706a1e9..1664637eac56 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -280,6 +280,15 @@ public final class ContentCaptureManager { "service_explicitly_enabled"; /** + * Device config property used by {@code android.widget.AbsListView} to determine whether or + * not it should report the positions of its children to Content Capture. + * + * @hide + */ + public static final String DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN = + "report_list_view_children"; + + /** * Maximum number of events that are buffered before sent to the app. * * @hide diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 1cb96b15b8ef..1e1b9baaffdd 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -20,6 +20,7 @@ import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.TestApi; +import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; @@ -37,6 +38,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.StrictMode; import android.os.Trace; +import android.provider.DeviceConfig; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; @@ -65,6 +67,7 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewHierarchyEncoder; import android.view.ViewParent; +import android.view.ViewStructure; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -73,6 +76,9 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; +import android.view.autofill.AutofillId; +import android.view.contentcapture.ContentCaptureManager; +import android.view.contentcapture.ContentCaptureSession; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; @@ -642,6 +648,23 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** + * Indicates that reporting positions of child views to content capture is enabled via + * DeviceConfig. + */ + private static boolean sContentCaptureReportingEnabledByDeviceConfig = false; + + /** + * Listens for changes to DeviceConfig properties and updates stored values accordingly. + */ + private static DeviceConfig.OnPropertiesChangedListener sDeviceConfigChangeListener = null; + + /** + * Indicates that child positions of views should be reported to Content Capture the next time + * that active views are refreshed. + */ + private boolean mReportChildrenToContentCaptureOnNextUpdate = true; + + /** * Helper object that renders and controls the fast scroll thumb. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768941) @@ -858,8 +881,44 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public void adjustListItemSelectionBounds(Rect bounds); } + private static class DeviceConfigChangeListener + implements DeviceConfig.OnPropertiesChangedListener { + @Override + public void onPropertiesChanged( + @NonNull DeviceConfig.Properties properties) { + if (!DeviceConfig.NAMESPACE_CONTENT_CAPTURE.equals(properties.getNamespace())) { + return; + } + + for (String key : properties.getKeyset()) { + if (!ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN + .equals(key)) { + continue; + } + + sContentCaptureReportingEnabledByDeviceConfig = properties.getBoolean(key, + false); + } + } + } + + private static void setupDeviceConfigProperties() { + if (sDeviceConfigChangeListener == null) { + sContentCaptureReportingEnabledByDeviceConfig = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_CONTENT_CAPTURE, + ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN, + false); + sDeviceConfigChangeListener = new DeviceConfigChangeListener(); + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_CONTENT_CAPTURE, + ActivityThread.currentApplication().getMainExecutor(), + sDeviceConfigChangeListener); + } + } + public AbsListView(Context context) { super(context); + setupDeviceConfigProperties(); mEdgeGlowBottom = new EdgeEffect(context); mEdgeGlowTop = new EdgeEffect(context); initAbsListView(); @@ -882,6 +941,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + setupDeviceConfigProperties(); mEdgeGlowBottom = new EdgeEffect(context, attrs); mEdgeGlowTop = new EdgeEffect(context, attrs); initAbsListView(); @@ -4802,6 +4862,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mOnScrollListener.onScrollStateChanged(this, newState); } } + + // When scrolling, we want to report changes in the active children to Content Capture, + // so set the flag to report on the next update only when scrolling has stopped or a fling + // scroll is performed. + if (newState == OnScrollListener.SCROLL_STATE_IDLE + || newState == OnScrollListener.SCROLL_STATE_FLING) { + mReportChildrenToContentCaptureOnNextUpdate = true; + } } /** @@ -6763,10 +6831,77 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mRecycler.mRecyclerListener = listener; } + /** + * {@inheritDoc} + * + * This method will initialize the fields of the {@link ViewStructure} + * using the base implementation in {@link View}. On API level 33 and higher, it may also + * write information about the positions of active views to the extras bundle provided by the + * {@link ViewStructure}. + * + * NOTE: When overriding this method on API level 33, if not calling super() or if changing the + * logic for child views, be sure to provide values for the first active child view position and + * the list of active child views in the {@link ViewStructure}'s extras {@link Bundle} using the + * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" and + * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" keys. + * + * @param structure {@link ViewStructure} to be filled in with structured view data. + * @param flags optional flags. + * + * @see View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + */ + @Override + public void onProvideContentCaptureStructure( + @NonNull ViewStructure structure, int flags) { + super.onProvideContentCaptureStructure(structure, flags); + if (!sContentCaptureReportingEnabledByDeviceConfig) { + return; + } + + Bundle extras = structure.getExtras(); + + if (extras == null) { + Log.wtf(TAG, "Unexpected null extras Bundle in ViewStructure"); + return; + } + + int childCount = getChildCount(); + ArrayList<AutofillId> idsList = new ArrayList<>(childCount); + + for (int i = 0; i < childCount; ++i) { + View activeView = getChildAt(i); + if (activeView == null) { + continue; + } + + idsList.add(activeView.getAutofillId()); + } + + extras.putParcelableArrayList(ViewStructure.EXTRA_ACTIVE_CHILDREN_IDS, + idsList); + + extras.putInt(ViewStructure.EXTRA_FIRST_ACTIVE_POSITION, + getFirstVisiblePosition()); + } + + private void reportActiveViewsToContentCapture() { + if (!sContentCaptureReportingEnabledByDeviceConfig) { + return; + } + + ContentCaptureSession session = getContentCaptureSession(); + if (session != null) { + ViewStructure structure = session.newViewStructure(this); + onProvideContentCaptureStructure(structure, /* flags= */ 0); + session.notifyViewAppeared(structure); + } + } + class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver { @Override public void onChanged() { super.onChanged(); + mReportChildrenToContentCaptureOnNextUpdate = true; if (mFastScroll != null) { mFastScroll.onSectionsChanged(); } @@ -6775,6 +6910,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public void onInvalidated() { super.onInvalidated(); + mReportChildrenToContentCaptureOnNextUpdate = true; if (mFastScroll != null) { mFastScroll.onSectionsChanged(); } @@ -7093,6 +7229,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te lp.scrappedFromPosition = firstActivePosition + i; } } + + if (mReportChildrenToContentCaptureOnNextUpdate && childCount > 0) { + AbsListView.this.reportActiveViewsToContentCapture(); + mReportChildrenToContentCaptureOnNextUpdate = false; + } } /** |