Implemented nested Content Capture sessions.

Test: atest CtsContentCaptureServiceTestCases
Fixes: 121033016

Change-Id: I46bbd05c363cbda8b66704203455411d38c6a025
diff --git a/core/java/android/view/contentcapture/ChildContentCaptureSession.java b/core/java/android/view/contentcapture/ChildContentCaptureSession.java
index 5166831..04e725e 100644
--- a/core/java/android/view/contentcapture/ChildContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ChildContentCaptureSession.java
@@ -33,7 +33,7 @@
 final class ChildContentCaptureSession extends ContentCaptureSession {
 
     @NonNull
-    private final MainContentCaptureSession mParent;
+    private final ContentCaptureSession mParent;
 
     /**
      * {@link ContentCaptureContext} set by client, or {@code null} when it's the
@@ -46,16 +46,25 @@
     private final ContentCaptureContext mClientContext;
 
     /** @hide */
-    protected ChildContentCaptureSession(@NonNull MainContentCaptureSession parent,
+    protected ChildContentCaptureSession(@NonNull ContentCaptureSession parent,
             @NonNull ContentCaptureContext clientContext) {
         mParent = parent;
         mClientContext = Preconditions.checkNotNull(clientContext);
     }
 
     @Override
-    ContentCaptureSession newChild(@NonNull ContentCaptureContext context) {
-        // TODO(b/121033016): implement it
-        throw new UnsupportedOperationException("grand-children not implemented yet");
+    MainContentCaptureSession getMainCaptureSession() {
+        if (mParent instanceof MainContentCaptureSession) {
+            return (MainContentCaptureSession) mParent;
+        }
+        return mParent.getMainCaptureSession();
+    }
+
+    @Override
+    ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
+        final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
+        getMainCaptureSession().notifyChildSessionStarted(mId, child.mId, clientContext);
+        return child;
     }
 
     @Override
@@ -65,27 +74,27 @@
 
     @Override
     void onDestroy() {
-        mParent.notifyChildSessionFinished(mParent.mId, mId);
+        getMainCaptureSession().notifyChildSessionFinished(mParent.mId, mId);
     }
 
     @Override
     void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
-        mParent.notifyViewAppeared(mId, node);
+        getMainCaptureSession().notifyViewAppeared(mId, node);
     }
 
     @Override
     void internalNotifyViewDisappeared(@NonNull AutofillId id) {
-        mParent.notifyViewDisappeared(mId, id);
+        getMainCaptureSession().notifyViewDisappeared(mId, id);
     }
 
     @Override
     void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
             int flags) {
-        mParent.notifyViewTextChanged(mId, id, text, flags);
+        getMainCaptureSession().notifyViewTextChanged(mId, id, text, flags);
     }
     @Override
     boolean isContentCaptureEnabled() {
-        return mParent.isContentCaptureEnabled();
+        return getMainCaptureSession().isContentCaptureEnabled();
     }
 
     @Override
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 9830790..e962e7c 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -117,13 +117,11 @@
     /** @hide */
     public void onActivityStarted(@NonNull IBinder applicationToken,
             @NonNull ComponentName activityComponent) {
-        // TODO(b/121033016): must start all sessions
         getMainContentCaptureSession().start(applicationToken, activityComponent);
     }
 
     /** @hide */
     public void onActivityStopped() {
-        // TODO(b/121033016): must finish all sessions
         getMainContentCaptureSession().destroy();
     }
 
@@ -135,7 +133,6 @@
      * @hide
      */
     public void flush() {
-        // TODO(b/121033016): must flush all sessions
         getMainContentCaptureSession().flush();
     }
 
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 293ba83..344b9973 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -27,6 +27,7 @@
 import android.view.autofill.AutofillId;
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
 import dalvik.system.CloseGuard;
@@ -34,7 +35,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Session used to notify a system-provided Content Capture service about events associated with
@@ -90,11 +90,14 @@
 
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
+    private final Object mLock = new Object();
+
     /**
      * Guard use to ignore events after it's destroyed.
      */
     @NonNull
-    private final AtomicBoolean mDestroyed = new AtomicBoolean();
+    @GuardedBy("mLock")
+    private boolean mDestroyed;
 
     /** @hide */
     @Nullable
@@ -108,11 +111,8 @@
     /**
      * List of children session.
      */
-    // TODO(b/121033016): need to synchonize access, either by changing on handler or UI thread
-    // (for now there's no handler on this class, so we need to wait for the next refactoring),
-    // most likely the former (as we have no guarantee that createContentCaptureSession()
-    // it will be called in the UiThread; for example, WebView most likely won't call on it)
     @Nullable
+    @GuardedBy("mLock")
     private ArrayList<ContentCaptureSession> mChildren;
 
     /** @hide */
@@ -120,6 +120,10 @@
         mCloseGuard.open("destroy");
     }
 
+    /** @hide */
+    @NonNull
+    abstract MainContentCaptureSession getMainCaptureSession();
+
     /**
      * Gets the id used to identify this session.
      */
@@ -143,10 +147,12 @@
             Log.d(TAG, "createContentCaptureSession(" + context + ": parent=" + mId + ", child="
                     + child.mId);
         }
-        if (mChildren == null) {
-            mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY);
+        synchronized (mLock) {
+            if (mChildren == null) {
+                mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY);
+            }
+            mChildren.add(child);
         }
-        mChildren.add(child);
         return child;
     }
 
@@ -163,29 +169,31 @@
      * <p>Once destroyed, any new notification will be dropped.
      */
     public final void destroy() {
-        if (!mDestroyed.compareAndSet(false, true)) {
-            Log.e(TAG, "destroy(" + mId + "): already destroyed");
-            return;
-        }
+        synchronized (mLock) {
+            if (mDestroyed) {
+                Log.e(TAG, "destroy(" + mId + "): already destroyed");
+                return;
+            }
+            mDestroyed = true;
 
-        mCloseGuard.close();
+            mCloseGuard.close();
 
-        //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
-        // id) and send it to the cache of batched commands
-        if (VERBOSE) {
-            Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
-        }
-
-        // Finish children first
-        if (mChildren != null) {
-            final int numberChildren = mChildren.size();
-            if (VERBOSE) Log.v(TAG, "Destroying " + numberChildren + " children first");
-            for (int i = 0; i < numberChildren; i++) {
-                final ContentCaptureSession child = mChildren.get(i);
-                try {
-                    child.destroy();
-                } catch (Exception e) {
-                    Log.w(TAG, "exception destroying child session #" + i + ": " + e);
+            //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
+            // id) and send it to the cache of batched commands
+            if (VERBOSE) {
+                Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
+            }
+            // Finish children first
+            if (mChildren != null) {
+                final int numberChildren = mChildren.size();
+                if (VERBOSE) Log.v(TAG, "Destroying " + numberChildren + " children first");
+                for (int i = 0; i < numberChildren; i++) {
+                    final ContentCaptureSession child = mChildren.get(i);
+                    try {
+                        child.destroy();
+                    } catch (Exception e) {
+                        Log.w(TAG, "exception destroying child session #" + i + ": " + e);
+                    }
                 }
             }
         }
@@ -305,23 +313,26 @@
     }
 
     boolean isContentCaptureEnabled() {
-        return !mDestroyed.get();
+        synchronized (mLock) {
+            return !mDestroyed;
+        }
     }
 
     @CallSuper
     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
         pw.print(prefix); pw.print("id: "); pw.println(mId);
-        pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed.get());
-        if (mChildren != null && !mChildren.isEmpty()) {
-            final String prefix2 = prefix + "  ";
-            final int numberChildren = mChildren.size();
-            pw.print(prefix); pw.print("number children: "); pw.println(numberChildren);
-            for (int i = 0; i < numberChildren; i++) {
-                final ContentCaptureSession child = mChildren.get(i);
-                pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw);
+        synchronized (mLock) {
+            pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
+            if (mChildren != null && !mChildren.isEmpty()) {
+                final String prefix2 = prefix + "  ";
+                final int numberChildren = mChildren.size();
+                pw.print(prefix); pw.print("number children: "); pw.println(numberChildren);
+                for (int i = 0; i < numberChildren; i++) {
+                    final ContentCaptureSession child = mChildren.get(i);
+                    pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw);
+                }
             }
         }
-
     }
 
     @Override
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index c23c85a..44a381e 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -141,6 +141,11 @@
     }
 
     @Override
+    MainContentCaptureSession getMainCaptureSession() {
+        return this;
+    }
+
+    @Override
     ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
         final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
         notifyChildSessionStarted(mId, child.mId, clientContext);
@@ -389,14 +394,14 @@
         handleResetSession(/* resetState= */ true);
     }
 
-    // TODO(b/121033016): once we support multiple sessions, we might need to move some of these
+    // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
     // clearings out.
     private void handleResetSession(boolean resetState) {
         if (resetState) {
             mState = STATE_UNKNOWN;
         }
 
-        // TODO(b/121033016): must reset children (which currently is owned by superclass)
+        // TODO(b/122454205): must reset children (which currently is owned by superclass)
         mApplicationToken = null;
         mComponentName = null;
         mEvents = null;
@@ -429,7 +434,7 @@
                 && !mDisabled.get();
     }
 
-    // TODO(b/121033016): refactor "notifyXXXX" methods below to a common "Buffer" object that is
+    // TODO(b/122454205): refactor "notifyXXXX" methods below to a common "Buffer" object that is
     // shared between ActivityContentCaptureSession and ChildContentCaptureSession objects. Such
     // change should also get get rid of the "internalNotifyXXXX" methods above
     void notifyChildSessionStarted(@NonNull String parentSessionId,