Moved createContentCaptureSession() to ContentCaptureSession.

Such move will allow nested sessions. For example, WebView could create a
new session for the main page, then child sessions for IFRAMEs contained on it.

This CL changes the API and provides an initial implementation, although it's
not quite ready yet - it only allows 1 level of children (from the activity
session), but the full implementation is coming soom to a movie theather near
you...

Bug: 121033016
Bug: 117944706

Test: atest CtsContentCaptureServiceTestCases

Change-Id: I86156bb3b8a2c08cb00b9518599eb6d67fbf77c2
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index c5e62f1..64f2355 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -34,13 +34,13 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
-import android.view.contentcapture.ActivityContentCaptureSession;
 import android.view.contentcapture.ContentCaptureContext;
 import android.view.contentcapture.ContentCaptureEvent;
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.ContentCaptureSession;
 import android.view.contentcapture.ContentCaptureSessionId;
 import android.view.contentcapture.IContentCaptureDirectManager;
+import android.view.contentcapture.MainContentCaptureSession;
 
 import com.android.internal.os.IResultReceiver;
 
@@ -293,13 +293,26 @@
         final List<ContentCaptureEvent> events = parceledEvents.getList();
         for (int i = 0; i < events.size(); i++) {
             final ContentCaptureEvent event = events.get(i);
+            if (!handleIsRightCallerFor(event, uid)) continue;
             String sessionIdString = event.getSessionId();
             if (!sessionIdString.equals(lastSessionId)) {
-                if (!handleIsRightCallerFor(sessionIdString, uid)) continue;
                 sessionId = new ContentCaptureSessionId(sessionIdString);
                 lastSessionId = sessionIdString;
             }
-            onContentCaptureEvent(sessionId, event);
+            switch (event.getType()) {
+                case ContentCaptureEvent.TYPE_SESSION_STARTED:
+                    final ContentCaptureContext clientContext = event.getClientContext();
+                    clientContext.setParentSessionId(event.getParentSessionId());
+                    mSessionsByUid.put(sessionIdString, uid);
+                    onCreateContentCaptureSession(clientContext, sessionId);
+                    break;
+                case ContentCaptureEvent.TYPE_SESSION_FINISHED:
+                    mSessionsByUid.remove(sessionIdString);
+                    onDestroyContentCaptureSession(sessionId);
+                    break;
+                default:
+                    onContentCaptureEvent(sessionId, event);
+            }
         }
     }
 
@@ -314,9 +327,18 @@
     }
 
     /**
-     * Checks if the given {@code uid} owns the session.
+     * Checks if the given {@code uid} owns the session associated with the event.
      */
-    private boolean handleIsRightCallerFor(@NonNull String sessionId, int uid) {
+    private boolean handleIsRightCallerFor(@NonNull ContentCaptureEvent event, int uid) {
+        final String sessionId;
+        switch (event.getType()) {
+            case ContentCaptureEvent.TYPE_SESSION_STARTED:
+            case ContentCaptureEvent.TYPE_SESSION_FINISHED:
+                sessionId = event.getParentSessionId();
+                break;
+            default:
+                sessionId = event.getSessionId();
+        }
         final Integer rightUid = mSessionsByUid.get(sessionId);
         if (rightUid == null) {
             if (VERBOSE) Log.v(TAG, "No session for " + sessionId);
@@ -347,7 +369,7 @@
             final Bundle extras;
             if (binder != null) {
                 extras = new Bundle();
-                extras.putBinder(ActivityContentCaptureSession.EXTRA_BINDER, binder);
+                extras.putBinder(MainContentCaptureSession.EXTRA_BINDER, binder);
             } else {
                 extras = null;
             }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 468d922..69cd3e6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -9046,17 +9046,16 @@
      * {@code onCreate()} and associate it with the root view of the activity:
      *
      * <pre>
-     *  ContentCaptureManager mgr = getSystemService(ContentCaptureManager.class);
-     *  if (mgr != null && mgr.isContentCaptureEnabled()) {
-     *    View rootView = findViewById(R.my_root_view);
-     *    ContentCaptureSession session = mgr.createContentCaptureSession(new
+     *   ContentCaptureSession oldSession = rootView.getContentCaptureSession();
+     *   if (oldSession != null) {
+     *     ContentCaptureSession newSession = oldSession.createContentCaptureSession(new
      *        ContentCaptureContext.Builder().setUri(myUrl).build());
-     *    rootView.setContentCaptureSession(session);
+     *     rootView.setContentCaptureSession(newSession);
      *  }
      * </pre>
      *
      * @param contentCaptureSession a session created by
-     * {@link ContentCaptureManager#createContentCaptureSession(
+     * {@link ContentCaptureSession#createContentCaptureSession(
      *        android.view.contentcapture.ContentCaptureContext)}.
      */
     public void setContentCaptureSession(@NonNull ContentCaptureSession contentCaptureSession) {
diff --git a/core/java/android/view/contentcapture/ChildContentCaptureSession.java b/core/java/android/view/contentcapture/ChildContentCaptureSession.java
new file mode 100644
index 0000000..5166831
--- /dev/null
+++ b/core/java/android/view/contentcapture/ChildContentCaptureSession.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.view.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+
+/**
+ * A session that is explicitly created by the app (and hence is a descendant of
+ * {@link MainContentCaptureSession}).
+ *
+ * @hide
+ */
+final class ChildContentCaptureSession extends ContentCaptureSession {
+
+    @NonNull
+    private final MainContentCaptureSession mParent;
+
+    /**
+     * {@link ContentCaptureContext} set by client, or {@code null} when it's the
+     * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
+     * context.
+     *
+     * @hide
+     */
+    @NonNull
+    private final ContentCaptureContext mClientContext;
+
+    /** @hide */
+    protected ChildContentCaptureSession(@NonNull MainContentCaptureSession 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");
+    }
+
+    @Override
+    void flush() {
+        mParent.flush();
+    }
+
+    @Override
+    void onDestroy() {
+        mParent.notifyChildSessionFinished(mParent.mId, mId);
+    }
+
+    @Override
+    void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
+        mParent.notifyViewAppeared(mId, node);
+    }
+
+    @Override
+    void internalNotifyViewDisappeared(@NonNull AutofillId id) {
+        mParent.notifyViewDisappeared(mId, id);
+    }
+
+    @Override
+    void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+            int flags) {
+        mParent.notifyViewTextChanged(mId, id, text, flags);
+    }
+    @Override
+    boolean isContentCaptureEnabled() {
+        return mParent.isContentCaptureEnabled();
+    }
+
+    @Override
+    void dump(String prefix, PrintWriter pw) {
+        if (mClientContext != null) {
+            // NOTE: we don't dump clientContent because it could have PII
+            pw.print(prefix); pw.println("hasClientContext");
+        }
+        super.dump(prefix, pw);
+    }
+}
diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java
index 9c11743..2d2987a 100644
--- a/core/java/android/view/contentcapture/ContentCaptureContext.java
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.java
@@ -21,6 +21,7 @@
 import android.annotation.SystemApi;
 import android.app.TaskInfo;
 import android.content.ComponentName;
+import android.content.Context;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -76,7 +77,7 @@
 
     /**
      * Flag indicating if this object has the app-provided context (which is set on
-     * {@link ContentCaptureManager#createContentCaptureSession(ContentCaptureContext)}).
+     * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}).
      */
     private final boolean mHasClientContext;
 
@@ -91,6 +92,9 @@
     private final int mDisplayId;
     private final int mFlags;
 
+    // Fields below are set by the service upon "delivery" and are not marshalled in the parcel
+    private @Nullable String mParentSessionId;
+
     /** @hide */
     public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
             @NonNull ComponentName componentName, int taskId, int displayId, int flags) {
@@ -153,16 +157,33 @@
     }
 
     /**
-     * Gets the activity associated with this context.
+     * Gets the activity associated with this context, or {@code null} when it is a child session.
      *
      * @hide
      */
     @SystemApi
-    public @NonNull ComponentName getActivityComponent() {
+    public @Nullable ComponentName getActivityComponent() {
         return mComponentName;
     }
 
     /**
+     * Gets the id of the session that originated this session (through
+     * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}),
+     * or {@code null} if this is the main session associated with the Activity's {@link Context}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public @Nullable ContentCaptureSessionId getParentSessionId() {
+        return mParentSessionId == null ?  null : new ContentCaptureSessionId(mParentSessionId);
+    }
+
+    /** @hide */
+    public void setParentSessionId(@NonNull String parentSessionId) {
+        mParentSessionId = parentSessionId;
+    }
+
+    /**
      * Gets the ID of the display associated with this context, as defined by
      * {G android.hardware.display.DisplayManager#getDisplay(int) DisplayManager.getDisplay()}.
      *
@@ -242,6 +263,9 @@
         pw.print("comp="); pw.print(ComponentName.flattenToShortString(mComponentName));
         pw.print(", taskId="); pw.print(mTaskId);
         pw.print(", displayId="); pw.print(mDisplayId);
+        if (mParentSessionId != null) {
+            pw.print(", parentId="); pw.print(mParentSessionId);
+        }
         if (mFlags > 0) {
             pw.print(", flags="); pw.print(mFlags);
         }
@@ -262,6 +286,9 @@
                 .append(", taskId=").append(mTaskId)
                 .append(", displayId=").append(mDisplayId)
                 .append(", flags=").append(mFlags);
+        if (mParentSessionId != null) {
+            builder.append(", parentId=").append(mParentSessionId);
+        }
         if (mExtras != null) {
             // NOTE: cannot print because it could contain PII
             builder.append(", hasExtras");
@@ -320,9 +347,9 @@
                 final int taskId = parcel.readInt();
                 final int displayId = parcel.readInt();
                 final int flags = parcel.readInt();
-                        return new ContentCaptureContext(clientContext, componentName, taskId,
-                                displayId, flags);
-                    }
+                return new ContentCaptureContext(clientContext, componentName, taskId, displayId,
+                        flags);
+            }
         }
 
         @Override
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index 43c9699..9e3da92 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -34,9 +34,9 @@
 public final class ContentCaptureEvent implements Parcelable {
 
     /** @hide */
-    public static final int TYPE_ACTIVITY_DESTROYED = -2;
+    public static final int TYPE_SESSION_FINISHED = -2;
     /** @hide */
-    public static final int TYPE_ACTIVITY_CREATED = -1;
+    public static final int TYPE_SESSION_STARTED = -1;
 
     /**
      * Called when a node has been added to the screen and is visible to the user.
@@ -78,6 +78,8 @@
     private @Nullable AutofillId mId;
     private @Nullable ViewNode mNode;
     private @Nullable CharSequence mText;
+    private @Nullable String mParentSessionId;
+    private @Nullable ContentCaptureContext mClientContext;
 
     /** @hide */
     public ContentCaptureEvent(@NonNull String sessionId, int type, long eventTime, int flags) {
@@ -103,12 +105,52 @@
         return this;
     }
 
+    /**
+     * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+     *
+     * @hide
+     */
+    public ContentCaptureEvent setParentSessionId(@NonNull String parentSessionId) {
+        mParentSessionId = parentSessionId;
+        return this;
+    }
+
+    /**
+     * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+     *
+     * @hide
+     */
+    public ContentCaptureEvent setClientContext(@NonNull ContentCaptureContext clientContext) {
+        mClientContext = clientContext;
+        return this;
+    }
+
     /** @hide */
     @NonNull
     public String getSessionId() {
         return mSessionId;
     }
 
+    /**
+     * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getParentSessionId() {
+        return mParentSessionId;
+    }
+
+    /**
+     * Used by {@link #TYPE_SESSION_STARTED}.
+     *
+     * @hide
+     */
+    @Nullable
+    public ContentCaptureContext getClientContext() {
+        return mClientContext;
+    }
+
     /** @hide */
     @NonNull
     public ContentCaptureEvent setViewNode(@NonNull ViewNode node) {
@@ -191,7 +233,17 @@
             pw.print(", id="); pw.print(mId);
         }
         if (mNode != null) {
-            pw.print(", id="); pw.print(mNode.getAutofillId());
+            pw.print(", mNode.id="); pw.print(mNode.getAutofillId());
+        }
+        if (mSessionId != null) {
+            pw.print(", sessionId="); pw.print(mSessionId);
+        }
+        if (mParentSessionId != null) {
+            pw.print(", parentSessionId="); pw.print(mParentSessionId);
+        }
+        if (mText != null) {
+            // Cannot print content because could have PII
+            pw.print(", text="); pw.print(mText.length()); pw.print("_chars");
         }
     }
 
@@ -229,6 +281,12 @@
         parcel.writeParcelable(mId, flags);
         ViewNode.writeToParcel(parcel, mNode, flags);
         parcel.writeCharSequence(mText);
+        if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) {
+            parcel.writeString(mParentSessionId);
+        }
+        if (mType == TYPE_SESSION_STARTED) {
+            parcel.writeParcelable(mClientContext, flags);
+        }
     }
 
     public static final Parcelable.Creator<ContentCaptureEvent> CREATOR =
@@ -251,6 +309,12 @@
                 event.setViewNode(node);
             }
             event.setText(parcel.readCharSequence());
+            if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) {
+                event.setParentSessionId(parcel.readString());
+            }
+            if (type == TYPE_SESSION_STARTED) {
+                event.setClientContext(parcel.readParcelable(null));
+            }
             return event;
         }
 
@@ -263,6 +327,10 @@
     /** @hide */
     public static String getTypeAsString(@EventType int type) {
         switch (type) {
+            case TYPE_SESSION_STARTED:
+                return "SESSION_STARTED";
+            case TYPE_SESSION_FINISHED:
+                return "SESSION_FINISHED";
             case TYPE_VIEW_APPEARED:
                 return "VIEW_APPEARED";
             case TYPE_VIEW_DISAPPEARED:
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index fca2857..9830790 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -18,13 +18,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
+import android.annotation.UiThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.util.Log;
-import android.view.View;
 
 import com.android.internal.util.Preconditions;
 
@@ -66,7 +66,7 @@
     @NonNull
     private final Handler mHandler;
 
-    private ActivityContentCaptureSession mMainSession;
+    private MainContentCaptureSession mMainSession;
 
     /** @hide */
     public ContentCaptureManager(@NonNull Context context,
@@ -93,46 +93,20 @@
     }
 
     /**
-     * Creates a new {@link ContentCaptureSession}.
-     *
-     * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info.
-     */
-    @NonNull
-    public ContentCaptureSession createContentCaptureSession(
-            @NonNull ContentCaptureContext context) {
-        if (DEBUG) Log.d(TAG, "createContentCaptureSession(): " + context);
-        // TODO(b/121033016): for now we're updating the main session, but we need instead:
-        // 1.Keep a list of sessions
-        // 2.Making sure the applicationToken and componentName passed by
-        // the activity is used on all of these sessions
-        // 3.We might also need to delay the start of all of these sessions until
-        // onActivityStarted() is called (and the main session is created).
-        // 4.Close (and delete) these sessions when onActivityStopped() is called.
-        // 5.Figure out whether each session will have its own mDisabled AtomicBoolean.
-        if (mMainSession == null) {
-            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
-                    mDisabled, Preconditions.checkNotNull(context));
-        } else {
-            throw new IllegalStateException("Manager already has a session: " + mMainSession);
-        }
-        return mMainSession;
-    }
-
-    /**
      * Gets the main session associated with the context.
      *
      * <p>By default there's just one (associated with the activity lifecycle), but apps could
-     * explicitly add more using {@link #createContentCaptureSession(ContentCaptureContext)}.
+     * explicitly add more using
+     * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}.
      *
      * @hide
      */
     @NonNull
-    public ActivityContentCaptureSession getMainContentCaptureSession() {
-        // TODO(b/121033016): figure out how to manage the "default" session when it support
-        // multiple sessions (can't just be the first one, as it could be closed).
+    @UiThread
+    public MainContentCaptureSession getMainContentCaptureSession() {
         if (mMainSession == null) {
-            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
-                    mDisabled, /* clientContext= */ null);
+            mMainSession = new MainContentCaptureSession(mContext, mHandler, mService,
+                    mDisabled);
             if (VERBOSE) {
                 Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
             }
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index aedb7a9..9f666a4 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -15,8 +15,10 @@
  */
 package android.view.contentcapture;
 
+import static android.view.contentcapture.ContentCaptureManager.DEBUG;
 import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
 
+import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.Log;
@@ -30,6 +32,7 @@
 import dalvik.system.CloseGuard;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.UUID;
 
 /**
@@ -80,6 +83,8 @@
      */
     public static final int STATE_DISABLED_DUPLICATED_ID = 4;
 
+    private static final int INITIAL_CHILDREN_CAPACITY = 5;
+
     /** @hide */
     protected final String mTag = getClass().getSimpleName();
 
@@ -95,19 +100,17 @@
     private ContentCaptureSessionId mContentCaptureSessionId;
 
     /**
-     * {@link ContentCaptureContext} set by client, or {@code null} when it's the
-     * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
-     * context.
-     *
-     * @hide
+     * 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
-    // TODO(b/121042846): move to ChildContentCaptureSession.java
-    protected final ContentCaptureContext mClientContext;
+    private ArrayList<ContentCaptureSession> mChildren;
 
     /** @hide */
-    protected ContentCaptureSession(@Nullable ContentCaptureContext clientContext) {
-        mClientContext = clientContext;
+    protected ContentCaptureSession() {
         mCloseGuard.open("destroy");
     }
 
@@ -122,6 +125,28 @@
     }
 
     /**
+     * Creates a new {@link ContentCaptureSession}.
+     *
+     * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info.
+     */
+    @NonNull
+    public final ContentCaptureSession createContentCaptureSession(
+            @NonNull ContentCaptureContext context) {
+        final ContentCaptureSession child = newChild(context);
+        if (DEBUG) {
+            Log.d(mTag, "createContentCaptureSession(" + context + ": parent=" + mId + ", child= "
+                    + child.mId);
+        }
+        if (mChildren == null) {
+            mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY);
+        }
+        mChildren.add(child);
+        return child;
+    }
+
+    abstract ContentCaptureSession newChild(@NonNull ContentCaptureContext context);
+
+    /**
      * Flushes the buffered events to the service.
      */
     abstract void flush();
@@ -134,24 +159,40 @@
     public final void destroy() {
         //TODO(b/111276913): mark it as destroyed so other methods are ignored (and test on CTS)
 
+        //TODO(b/111276913): probably shouldn't check for it
         if (!isContentCaptureEnabled()) return;
 
+        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(mTag, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
         }
 
-        flush();
+        // Finish children first
+        if (mChildren != null) {
+            final int numberChildren = mChildren.size();
+            if (VERBOSE) Log.v(mTag, "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(mTag, "exception destroying child session #" + i + ": " + e);
+                }
+            }
+        }
 
-        onDestroy();
-
-        mCloseGuard.close();
+        try {
+            flush();
+        } finally {
+            onDestroy();
+        }
     }
 
     abstract void onDestroy();
 
-
     /** @hide */
     @Override
     public void close() {
@@ -259,7 +300,19 @@
 
     abstract boolean isContentCaptureEnabled();
 
-    abstract void dump(@NonNull String prefix, @NonNull PrintWriter pw);
+    @CallSuper
+    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+        if (mChildren != null && !mChildren.isEmpty()) {
+            final String prefix2 = prefix + "  ";
+            final int numberChildren = mChildren.size();
+            pw.print(prefix); pw.print("number children: "); pw.print(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
     public String toString() {
diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
index cbd3701..01776f8 100644
--- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
@@ -33,7 +33,6 @@
   */
 oneway interface IContentCaptureManager {
     void startSession(int userId, IBinder activityToken, in ComponentName componentName,
-                      String sessionId, in ContentCaptureContext clientContext, int flags,
-                      in IResultReceiver result);
+                      String sessionId, int flags, in IResultReceiver result);
     void finishSession(int userId, String sessionId);
 }
diff --git a/core/java/android/view/contentcapture/ActivityContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
similarity index 82%
rename from core/java/android/view/contentcapture/ActivityContentCaptureSession.java
rename to core/java/android/view/contentcapture/MainContentCaptureSession.java
index a8f3e0b..ea6f2fe 100644
--- a/core/java/android/view/contentcapture/ActivityContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -15,6 +15,8 @@
  */
 package android.view.contentcapture;
 
+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;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
@@ -59,7 +61,7 @@
  *
  * @hide
  */
-public final class ActivityContentCaptureSession extends ContentCaptureSession {
+public final class MainContentCaptureSession extends ContentCaptureSession {
 
     /**
      * Handler message used to flush the buffer.
@@ -129,18 +131,23 @@
     // Lazily created on demand.
     private ContentCaptureSessionId mContentCaptureSessionId;
 
-    /**
-     * @hide */
-    protected ActivityContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
-            @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled,
-            @Nullable ContentCaptureContext clientContext) {
-        super(clientContext);
+    /** @hide */
+    protected MainContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
+            @Nullable IContentCaptureManager systemServerInterface,
+            @NonNull AtomicBoolean disabled) {
         mContext = context;
         mHandler = handler;
         mSystemServerInterface = systemServerInterface;
         mDisabled = disabled;
     }
 
+    @Override
+    ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
+        final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
+        notifyChildSessionStarted(mId, child.mId, clientContext);
+        return child;
+    }
+
     /**
      * Starts this session.
      *
@@ -154,19 +161,19 @@
                     + ComponentName.flattenToShortString(activityComponent));
         }
 
-        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleStartSession, this,
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleStartSession, this,
                 applicationToken, activityComponent));
     }
 
     @Override
     void flush() {
-        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleForceFlush, this));
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleForceFlush, this));
     }
 
     @Override
     void onDestroy() {
         mHandler.sendMessage(
-                obtainMessage(ActivityContentCaptureSession::handleDestroySession, this));
+                obtainMessage(MainContentCaptureSession::handleDestroySession, this));
     }
 
     private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
@@ -188,7 +195,7 @@
 
         try {
             mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
-                    componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
+                    componentName, mId, flags, new IResultReceiver.Stub() {
                         @Override
                         public void send(int resultCode, Bundle resultData) {
                             IBinder binder = null;
@@ -296,7 +303,7 @@
             Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
         }
         mHandler.sendMessageDelayed(
-                obtainMessage(ActivityContentCaptureSession::handleFlushIfNeeded, this)
+                obtainMessage(MainContentCaptureSession::handleFlushIfNeeded, this)
                 .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
     }
 
@@ -312,7 +319,7 @@
         if (mEvents == null) return;
 
         if (mDirectServiceInterface == null) {
-            Log.w(mTag, "handleForceFlush(): client not available yet");
+            if (DEBUG) Log.d(mTag, "handleForceFlush(): hold your horses, client not ready yet!");
             if (!mHandler.hasMessages(MSG_FLUSH)) {
                 handleScheduleFlush(/* checkExisting= */ false);
             }
@@ -367,12 +374,15 @@
         handleResetSession(/* resetState= */ true);
     }
 
-    // TODO(b/121042846): once we support multiple sessions, we might need to move some of these
+    // TODO(b/121033016): 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)
+
         mContentCaptureSessionId = null;
         mApplicationToken = null;
         mComponentName = null;
@@ -386,24 +396,18 @@
 
     @Override
     void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
-        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(mId, TYPE_VIEW_APPEARED)
-                        .setViewNode(node.mNode), /* forceFlush= */ false));
+        notifyViewAppeared(mId, node);
     }
 
     @Override
     void internalNotifyViewDisappeared(@NonNull AutofillId id) {
-        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(mId, TYPE_VIEW_DISAPPEARED).setAutofillId(id),
-                        /* forceFlush= */ false));
+        notifyViewDisappeared(mId, id);
     }
 
     @Override
     void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
             int flags) {
-        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(mId, TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
-                        .setText(text), /* forceFlush= */ false));
+        notifyViewTextChanged(mId, id, text, flags);
     }
 
     @Override
@@ -411,6 +415,44 @@
         return mSystemServerInterface != null && !mDisabled.get();
     }
 
+    // TODO(b/121033016): 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,
+            @NonNull String childSessionId, @NonNull ContentCaptureContext clientContext) {
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+                        .setParentSessionId(parentSessionId)
+                        .setClientContext(clientContext),
+                        /* forceFlush= */ false));
+    }
+
+    void notifyChildSessionFinished(@NonNull String parentSessionId,
+            @NonNull String childSessionId) {
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+                        .setParentSessionId(parentSessionId), /* forceFlush= */ false));
+    }
+
+    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));
+    }
+
+    void notifyViewDisappeared(@NonNull String sessionId, @NonNull AutofillId id) {
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id),
+                        /* forceFlush= */ false));
+    }
+
+    void notifyViewTextChanged(@NonNull String sessionId, @NonNull AutofillId id,
+            @Nullable CharSequence text, int flags) {
+        mHandler.sendMessage(obtainMessage(MainContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
+                        .setText(text), /* forceFlush= */ false));
+    }
+
     @Override
     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
         pw.print(prefix); pw.print("id: "); pw.println(mId);
@@ -424,11 +466,6 @@
             pw.print(prefix); pw.print("mDirectServiceInterface: ");
             pw.println(mDirectServiceInterface);
         }
-        if (mClientContext != null) {
-            // NOTE: we don't dump clientContent because it could have PII
-            pw.print(prefix); pw.println("hasClientContext");
-
-        }
         pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
         pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
         if (mContentCaptureSessionId != null) {
@@ -459,6 +496,7 @@
             pw.print(prefix); pw.print("next flush: ");
             TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
         }
+        super.dump(prefix, pw);
     }
 
     /**