ContentCapture: add new API to notify views deletion on batch.

Test: atest CtsContentCaptureServiceTestCases
Bug: 121050915

Change-Id: I325cfb2954c7c990c8916fe9b17a2642dcaa1c79
diff --git a/api/system-current.txt b/api/system-current.txt
index eaca6ab..e40a350 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -8266,6 +8266,7 @@
     method public long getEventTime();
     method public int getFlags();
     method public android.view.autofill.AutofillId getId();
+    method public java.util.List<android.view.autofill.AutofillId> getIds();
     method public java.lang.CharSequence getText();
     method public int getType();
     method public android.view.contentcapture.ViewNode getViewNode();
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index 0d06430..a3b285d 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -21,6 +21,7 @@
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Log;
 import android.view.autofill.AutofillId;
 
 import com.android.internal.util.Preconditions;
@@ -28,11 +29,15 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
 
 /** @hide */
 @SystemApi
 public final class ContentCaptureEvent implements Parcelable {
 
+    private static final String TAG = ContentCaptureEvent.class.getSimpleName();
+
     /** @hide */
     public static final int TYPE_SESSION_FINISHED = -2;
     /** @hide */
@@ -46,9 +51,11 @@
     public static final int TYPE_VIEW_APPEARED = 1;
 
     /**
-     * Called when a node has been removed from the screen and is not visible to the user anymore.
+     * Called when one or more nodes have been removed from the screen and is not visible to the
+     * user anymore.
      *
-     * <p>The id of the node is available through {@link #getId()}.
+     * <p>To get the id(s), first call {@link #getIds()} - if it returns {@code null}, then call
+     * {@link #getId()}.
      */
     public static final int TYPE_VIEW_DISAPPEARED = 2;
 
@@ -76,6 +83,7 @@
     private final long mEventTime;
     private final int mFlags;
     private @Nullable AutofillId mId;
+    private @Nullable ArrayList<AutofillId> mIds;
     private @Nullable ViewNode mNode;
     private @Nullable CharSequence mText;
     private @Nullable String mParentSessionId;
@@ -105,6 +113,27 @@
         return this;
     }
 
+    private void setAutofillIds(@NonNull ArrayList<AutofillId> ids) {
+        mIds = Preconditions.checkNotNull(ids);
+    }
+
+    /**
+     * Adds an autofill id to the this event, merging the single id into a list if necessary.
+     * @hide */
+    public ContentCaptureEvent addAutofillId(@NonNull AutofillId id) {
+        if (mIds == null) {
+            mIds = new ArrayList<>();
+            if (mId == null) {
+                Log.w(TAG, "addAutofillId(" + id + ") called without an initial id");
+            } else {
+                mIds.add(mId);
+                mId = null;
+            }
+        }
+        mIds.add(id);
+        return this;
+    }
+
     /**
      * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
      *
@@ -205,7 +234,9 @@
     /**
      * Gets the {@link AutofillId} of the node associated with the event.
      *
-     * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED} and {@link #TYPE_VIEW_TEXT_CHANGED} events.
+     * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED} (when the event contains just one node - if
+     * it contains more than one, this method returns {@code null} and the actual ids should be
+     * retrived by {@link #getIds()}) and {@link #TYPE_VIEW_TEXT_CHANGED} events.
      */
     @Nullable
     public AutofillId getId() {
@@ -213,6 +244,17 @@
     }
 
     /**
+     * Gets the {@link AutofillId AutofillIds} of the nodes associated with the event.
+     *
+     * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED}, when the event contains more than one node
+     * (if it contains just one node, it's returned by {@link #getId()} instead.
+     */
+    @Nullable
+    public List<AutofillId> getIds() {
+        return mIds;
+    }
+
+    /**
      * Gets the current text of the node associated with the event.
      *
      * <p>Only set on {@link #TYPE_VIEW_TEXT_CHANGED} events.
@@ -232,6 +274,9 @@
         if (mId != null) {
             pw.print(", id="); pw.print(mId);
         }
+        if (mIds != null) {
+            pw.print(", ids="); pw.print(mIds);
+        }
         if (mNode != null) {
             pw.print(", mNode.id="); pw.print(mNode.getAutofillId());
         }
@@ -261,6 +306,9 @@
         if (mId != null) {
             string.append(", id=").append(mId);
         }
+        if (mIds != null) {
+            string.append(", ids=").append(mIds);
+        }
         if (mNode != null) {
             final String className = mNode.getClassName();
             if (mNode != null) {
@@ -283,6 +331,7 @@
         parcel.writeLong(mEventTime);
         parcel.writeInt(mFlags);
         parcel.writeParcelable(mId, flags);
+        parcel.writeTypedList(mIds);
         ViewNode.writeToParcel(parcel, mNode, flags);
         parcel.writeCharSequence(mText);
         if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) {
@@ -308,6 +357,10 @@
             if (id != null) {
                 event.setAutofillId(id);
             }
+            final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR);
+            if (ids != null) {
+                event.setAutofillIds(ids);
+            }
             final ViewNode node = ViewNode.readFromParcel(parcel);
             if (node != null) {
                 event.setViewNode(node);
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 7fb1fdd..21914e2 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -363,8 +363,8 @@
         Preconditions.checkArgument(!ArrayUtils.isEmpty(virtualIds), "virtual ids cannot be empty");
         if (!isContentCaptureEnabled()) return;
 
-        // TODO(b/121050915): create a new VIEWS_DISAPPEARED event type, which could also be used
-        // to batch delete non-virtual views
+        // TODO(b/123036895): use a internalNotifyViewsDisappeared that optimizes how the event is
+        // parcelized
         for (int id : virtualIds) {
             internalNotifyViewDisappeared(new AutofillId(hostId, id, getIdAsInt()));
         }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index f0778fa..af56af3 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -286,6 +286,9 @@
             mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
         }
 
+        // Some type of events can be merged together
+        boolean addEvent = true;
+
         if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) {
             final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
 
@@ -297,10 +300,24 @@
                             + event.getText());
                 }
                 lastEvent.setText(event.getText());
-            } else {
-                mEvents.add(event);
+                addEvent = false;
             }
-        } else {
+        }
+
+        if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
+            final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
+            if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
+                    && event.getSessionId().equals(lastEvent.getSessionId())) {
+                if (VERBOSE) {
+                    Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
+                            + lastEvent.getSessionId());
+                }
+                lastEvent.addAutofillId(event.getId());
+                addEvent = false;
+            }
+        }
+
+        if (addEvent) {
             mEvents.add(event);
         }