Split ContentCaptureSession in 2 classes.

This is just a refactoring for now, but it paves the way to support children
sessions.

Bug: 121042846
Bug: 117944706

Test: atest CtsContentCaptureServiceTestCases

Change-Id: I64bb5562dcfd4a9f0f69bb13009e4cf47a4f3b37
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index 953ccf1..5e87c40 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -34,6 +34,7 @@
 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;
@@ -323,7 +324,7 @@
             final Bundle extras;
             if (binder != null) {
                 extras = new Bundle();
-                extras.putBinder(ContentCaptureSession.EXTRA_BINDER, binder);
+                extras.putBinder(ActivityContentCaptureSession.EXTRA_BINDER, binder);
             } else {
                 extras = null;
             }
diff --git a/core/java/android/view/contentcapture/ActivityContentCaptureSession.java b/core/java/android/view/contentcapture/ActivityContentCaptureSession.java
new file mode 100644
index 0000000..7886518
--- /dev/null
+++ b/core/java/android/view/contentcapture/ActivityContentCaptureSession.java
@@ -0,0 +1,471 @@
+/*
+ * 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 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;
+import static android.view.contentcapture.ContentCaptureManager.DEBUG;
+import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.TimeUtils;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import com.android.internal.os.IResultReceiver;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main session associated with a context.
+ *
+ * <p>This session is created when the activity starts and finished when it stops; clients can use
+ * it to create children activities.
+ *
+ * <p><b>NOTE: all methods in this class should return right away, or do the real work in a handler
+ * thread. Hence, the only field that must be thread-safe is {@code mEnabled}, which is called at
+ * the beginning of every method.
+ *
+ * @hide
+ */
+public final class ActivityContentCaptureSession extends ContentCaptureSession {
+
+    /**
+     * Handler message used to flush the buffer.
+     */
+    private static final int MSG_FLUSH = 1;
+
+    /**
+     * Maximum number of events that are buffered before sent to the app.
+     */
+    // TODO(b/121044064): use settings
+    private static final int MAX_BUFFER_SIZE = 100;
+
+    /**
+     * Frequency the buffer is flushed if stale.
+     */
+    // TODO(b/121044064): use settings
+    private static final int FLUSHING_FREQUENCY_MS = 5_000;
+
+    /**
+     * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
+     * @hide
+     */
+    public static final String EXTRA_BINDER = "binder";
+
+    @NonNull
+    private final AtomicBoolean mDisabled;
+
+    @NonNull
+    private final Context mContext;
+
+    @NonNull
+    private final Handler mHandler;
+
+    /**
+     * Interface to the system_server binder object - it's only used to start the session (and
+     * notify when the session is finished).
+     */
+    @Nullable
+    private final IContentCaptureManager mSystemServerInterface;
+
+    /**
+     * Direct interface to the service binder object - it's used to send the events, including the
+     * last ones (when the session is finished)
+     */
+    @Nullable
+    private IContentCaptureDirectManager mDirectServiceInterface;
+    @Nullable
+    private DeathRecipient mDirectServiceVulture;
+
+    private int mState = STATE_UNKNOWN;
+
+    @Nullable
+    private IBinder mApplicationToken;
+
+    @Nullable
+    private ComponentName mComponentName;
+
+    /**
+     * List of events held to be sent as a batch.
+     */
+    @Nullable
+    private ArrayList<ContentCaptureEvent> mEvents;
+
+    // Used just for debugging purposes (on dump)
+    private long mNextFlush;
+
+    // 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);
+        mContext = context;
+        mHandler = handler;
+        mSystemServerInterface = systemServerInterface;
+        mDisabled = disabled;
+    }
+
+    /**
+     * Starts this session.
+     *
+     * @hide
+     */
+    void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
+        if (!isContentCaptureEnabled()) return;
+
+        if (VERBOSE) {
+            Log.v(mTag, "start(): token=" + applicationToken + ", comp="
+                    + ComponentName.flattenToShortString(activityComponent));
+        }
+
+        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleStartSession, this,
+                applicationToken, activityComponent));
+    }
+
+    @Override
+    void flush() {
+        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleForceFlush, this));
+    }
+
+    @Override
+    void onDestroy() {
+        mHandler.sendMessage(
+                obtainMessage(ActivityContentCaptureSession::handleDestroySession, this));
+    }
+
+    private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
+        if (mState != STATE_UNKNOWN) {
+            // TODO(b/111276913): revisit this scenario
+            Log.w(mTag, "ignoring handleStartSession(" + token + ") while on state "
+                    + getStateAsString(mState));
+            return;
+        }
+        mState = STATE_WAITING_FOR_SERVER;
+        mApplicationToken = token;
+        mComponentName = componentName;
+
+        if (VERBOSE) {
+            Log.v(mTag, "handleStartSession(): token=" + token + ", act="
+                    + getActivityDebugName() + ", id=" + mId);
+        }
+        final int flags = 0; // TODO(b/111276913): get proper flags
+
+        try {
+            mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
+                    componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
+                        @Override
+                        public void send(int resultCode, Bundle resultData) {
+                            IBinder binder = null;
+                            if (resultData != null) {
+                                binder = resultData.getBinder(EXTRA_BINDER);
+                                if (binder == null) {
+                                    Log.wtf(mTag, "No " + EXTRA_BINDER + " extra result");
+                                    handleResetState();
+                                    return;
+                                }
+                            }
+                            handleSessionStarted(resultCode, binder);
+                        }
+                    });
+        } catch (RemoteException e) {
+            Log.w(mTag, "Error starting session for " + componentName.flattenToShortString() + ": "
+                    + e);
+        }
+    }
+
+    /**
+     * Callback from {@code system_server} after call to
+     * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
+     * ContentCaptureContext, int, IResultReceiver)}.
+     *
+     * @param resultCode session state
+     * @param binder handle to {@code IContentCaptureDirectManager}
+     */
+    private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
+        mState = resultCode;
+        if (binder != null) {
+            mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
+            mDirectServiceVulture = () -> {
+                Log.w(mTag, "Destroying session " + mId + " because service died");
+                destroy();
+            };
+            try {
+                binder.linkToDeath(mDirectServiceVulture, 0);
+            } catch (RemoteException e) {
+                Log.w(mTag, "Failed to link to death on " + binder + ": " + e);
+            }
+        }
+        if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
+            mDisabled.set(true);
+            handleResetSession(/* resetState= */ false);
+        } else {
+            mDisabled.set(false);
+        }
+        if (VERBOSE) {
+            Log.v(mTag, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
+                    + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
+                    + ", binder=" + binder);
+        }
+    }
+
+    private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+        if (mEvents == null) {
+            if (VERBOSE) {
+                Log.v(mTag, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
+            }
+            mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
+        }
+        mEvents.add(event);
+
+        final int numberEvents = mEvents.size();
+
+        // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
+        // buffered (either total or per autofillid). For
+        // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
+        // "a" and "b" then send "abc".
+        final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
+
+        if (bufferEvent && !forceFlush) {
+            handleScheduleFlush(/* checkExisting= */ true);
+            return;
+        }
+
+        if (mState != STATE_ACTIVE) {
+            // Callback from startSession hasn't been called yet - typically happens on system
+            // apps that are started before the system service
+            // TODO(b/111276913): try to ignore session while system is not ready / boot
+            // not complete instead. Similarly, the manager service should return right away
+            // when the user does not have a service set
+            if (VERBOSE) {
+                Log.v(mTag, "Closing session for " + getActivityDebugName()
+                        + " after " + numberEvents + " delayed events and state "
+                        + getStateAsString(mState));
+            }
+            handleResetState();
+            // TODO(b/111276913): blacklist activity / use special flag to indicate that
+            // when it's launched again
+            return;
+        }
+
+        handleForceFlush();
+    }
+
+    private void handleScheduleFlush(boolean checkExisting) {
+        if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
+            // "Renew" the flush message by removing the previous one
+            mHandler.removeMessages(MSG_FLUSH);
+        }
+        mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
+        if (VERBOSE) {
+            Log.v(mTag, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
+        }
+        mHandler.sendMessageDelayed(
+                obtainMessage(ActivityContentCaptureSession::handleFlushIfNeeded, this)
+                .setWhat(MSG_FLUSH), FLUSHING_FREQUENCY_MS);
+    }
+
+    private void handleFlushIfNeeded() {
+        if (mEvents.isEmpty()) {
+            if (VERBOSE) Log.v(mTag, "Nothing to flush");
+            return;
+        }
+        handleForceFlush();
+    }
+
+    private void handleForceFlush() {
+        if (mEvents == null) return;
+
+        if (mDirectServiceInterface == null) {
+            Log.w(mTag, "handleForceFlush(): client not available yet");
+            if (!mHandler.hasMessages(MSG_FLUSH)) {
+                handleScheduleFlush(/* checkExisting= */ false);
+            }
+            return;
+        }
+
+        final int numberEvents = mEvents.size();
+        try {
+            if (DEBUG) {
+                Log.d(mTag, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
+            }
+            mHandler.removeMessages(MSG_FLUSH);
+
+            final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
+            mDirectServiceInterface.sendEvents(mId, events);
+        } catch (RemoteException e) {
+            Log.w(mTag, "Error sending " + numberEvents + " for " + getActivityDebugName()
+                    + ": " + e);
+        }
+    }
+
+    /**
+     * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
+     */
+    @NonNull
+    private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
+        // NOTE: we must save a reference to the current mEvents and then set it to to null,
+        // otherwise clearing it would clear it in the receiving side if the service is also local.
+        final List<ContentCaptureEvent> events = mEvents == null
+                ? Collections.emptyList()
+                : mEvents;
+        mEvents = null;
+        return new ParceledListSlice<>(events);
+    }
+
+    private void handleDestroySession() {
+        if (DEBUG) {
+            Log.d(mTag, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+                    + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+                    + getActivityDebugName());
+        }
+
+        try {
+            mSystemServerInterface.finishSession(mContext.getUserId(), mId);
+        } catch (RemoteException e) {
+            Log.e(mTag, "Error destroying system-service session " + mId + " for "
+                    + getActivityDebugName() + ": " + e);
+        }
+    }
+
+    private void handleResetState() {
+        handleResetSession(/* resetState= */ true);
+    }
+
+    // TODO(b/121042846): 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;
+        }
+        mContentCaptureSessionId = null;
+        mApplicationToken = null;
+        mComponentName = null;
+        mEvents = null;
+        if (mDirectServiceInterface != null) {
+            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+        }
+        mDirectServiceInterface = null;
+        mHandler.removeMessages(MSG_FLUSH);
+    }
+
+    @Override
+    void internalNotifyViewAppeared(@NonNull ViewStructureImpl node) {
+        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(TYPE_VIEW_APPEARED)
+                        .setViewNode(node.mNode), /* forceFlush= */ false));
+    }
+
+    @Override
+    void internalNotifyViewDisappeared(@NonNull AutofillId id) {
+        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
+                        /* forceFlush= */ false));
+    }
+
+    @Override
+    void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+            int flags) {
+        mHandler.sendMessage(obtainMessage(ActivityContentCaptureSession::handleSendEvent, this,
+                new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
+                        .setText(text), /* forceFlush= */ false));
+    }
+
+    @Override
+    boolean isContentCaptureEnabled() {
+        return mSystemServerInterface != null && !mDisabled.get();
+    }
+
+    @Override
+    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+        pw.print(prefix); pw.print("id: "); pw.println(mId);
+        pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
+        pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
+        if (mSystemServerInterface != null) {
+            pw.print(prefix); pw.print("mSystemServerInterface: ");
+            pw.println(mSystemServerInterface);
+        }
+        if (mDirectServiceInterface != null) {
+            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) {
+            pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
+        }
+        pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
+        pw.print(getStateAsString(mState)); pw.println(")");
+        if (mApplicationToken != null) {
+            pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
+        }
+        if (mComponentName != null) {
+            pw.print(prefix); pw.print("component name: ");
+            pw.println(mComponentName.flattenToShortString());
+        }
+        if (mEvents != null && !mEvents.isEmpty()) {
+            final int numberEvents = mEvents.size();
+            pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
+            pw.print('/'); pw.println(MAX_BUFFER_SIZE);
+            if (VERBOSE && numberEvents > 0) {
+                final String prefix3 = prefix + "  ";
+                for (int i = 0; i < numberEvents; i++) {
+                    final ContentCaptureEvent event = mEvents.get(i);
+                    pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
+                    pw.println();
+                }
+            }
+            pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
+            pw.print(prefix); pw.print("next flush: ");
+            TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
+        }
+    }
+
+    /**
+     * Gets a string that can be used to identify the activity on logging statements.
+     */
+    private String getActivityDebugName() {
+        return mComponentName == null ? mContext.getPackageName()
+                : mComponentName.flattenToShortString();
+    }
+}
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 7fbbfb7..fca2857 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -66,7 +66,7 @@
     @NonNull
     private final Handler mHandler;
 
-    private ContentCaptureSession mMainSession;
+    private ActivityContentCaptureSession mMainSession;
 
     /** @hide */
     public ContentCaptureManager(@NonNull Context context,
@@ -110,7 +110,7 @@
         // 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 ContentCaptureSession(mContext, mHandler, mService,
+            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
                     mDisabled, Preconditions.checkNotNull(context));
         } else {
             throw new IllegalStateException("Manager already has a session: " + mMainSession);
@@ -127,12 +127,12 @@
      * @hide
      */
     @NonNull
-    public ContentCaptureSession getMainContentCaptureSession() {
+    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).
         if (mMainSession == null) {
-            mMainSession = new ContentCaptureSession(mContext, mHandler, mService, mDisabled,
-                    /* contentCaptureContext=  */ null);
+            mMainSession = new ActivityContentCaptureSession(mContext, mHandler, mService,
+                    mDisabled, /* clientContext= */ null);
             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 f411cf7..aedb7a9 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -15,62 +15,32 @@
  */
 package android.view.contentcapture;
 
-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;
-import static android.view.contentcapture.ContentCaptureManager.DEBUG;
 import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
 
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ParceledListSlice;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.IBinder.DeathRecipient;
-import android.os.RemoteException;
-import android.os.SystemClock;
 import android.util.Log;
-import android.util.TimeUtils;
 import android.view.View;
 import android.view.ViewStructure;
 import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
 
-import com.android.internal.os.IResultReceiver;
 import com.android.internal.util.Preconditions;
 
 import dalvik.system.CloseGuard;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Session used to notify a system-provided Content Capture service about events associated with
  * views.
  */
-public final class ContentCaptureSession implements AutoCloseable {
-
-    /*
-     * IMPLEMENTATION NOTICE:
-     *
-     * All methods in this class should return right away, or do the real work in a handler thread.
-     *
-     * Hence, the only field that must be thread-safe is mEnabled, which is called at the
-     * beginning of every method.
-     */
-
-    private static final String TAG = ContentCaptureSession.class.getSimpleName();
+public abstract class ContentCaptureSession implements AutoCloseable {
 
     /**
      * Used on {@link #notifyViewTextChanged(AutofillId, CharSequence, int)} to indicate that the
+     *
      * thext change was caused by user input (for example, through IME).
      */
     public static final int FLAG_USER_INPUT = 0x1;
@@ -110,79 +80,17 @@
      */
     public static final int STATE_DISABLED_DUPLICATED_ID = 4;
 
-    /**
-     * Handler message used to flush the buffer.
-     */
-    private static final int MSG_FLUSH = 1;
-
-    /**
-     * Maximum number of events that are buffered before sent to the app.
-     */
-    // TODO(b/121044064): use settings
-    private static final int MAX_BUFFER_SIZE = 100;
-
-    /**
-     * Frequency the buffer is flushed if stale.
-     */
-    // TODO(b/121044064): use settings
-    private static final int FLUSHING_FREQUENCY_MS = 5_000;
-
-
-    /**
-     * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
-     * @hide
-     */
-    public static final String EXTRA_BINDER = "binder";
+    /** @hide */
+    protected final String mTag = getClass().getSimpleName();
 
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
-    @NonNull
-    private final AtomicBoolean mDisabled;
-
-    @NonNull
-    private final Context mContext;
-
-    @NonNull
-    private final Handler mHandler;
-
-    /**
-     * Interface to the system_server binder object - it's only used to start the session (and
-     * notify when the session is finished).
-     */
+    /** @hide */
     @Nullable
-    private final IContentCaptureManager mSystemServerInterface;
-
-    /**
-     * Direct interface to the service binder object - it's used to send the events, including the
-     * last ones (when the session is finished)
-     */
-    @Nullable
-    private IContentCaptureDirectManager mDirectServiceInterface;
-    @Nullable
-    private DeathRecipient mDirectServiceVulture;
-
-    @Nullable
-    private final String mId = UUID.randomUUID().toString();
+    protected final String mId = UUID.randomUUID().toString();
 
     private int mState = STATE_UNKNOWN;
 
-    @Nullable
-    private IBinder mApplicationToken;
-
-    @Nullable
-    private ComponentName mComponentName;
-
-    /**
-     * List of events held to be sent as a batch.
-     */
-    // TODO(b/111276913): once we support multiple sessions, we need to move the buffer of events
-    // to its own class so it's shared by all sessions
-    @Nullable
-    private ArrayList<ContentCaptureEvent> mEvents;
-
-    // Used just for debugging purposes (on dump)
-    private long mNextFlush;
-
     // Lazily created on demand.
     private ContentCaptureSessionId mContentCaptureSessionId;
 
@@ -190,18 +98,15 @@
      * {@link ContentCaptureContext} set by client, or {@code null} when it's the
      * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
      * context.
+     *
+     * @hide
      */
     @Nullable
-    private final ContentCaptureContext mClientContext;
+    // TODO(b/121042846): move to ChildContentCaptureSession.java
+    protected final ContentCaptureContext mClientContext;
 
     /** @hide */
-    protected ContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
-            @Nullable IContentCaptureManager systemServerInterface, @NonNull AtomicBoolean disabled,
-            @Nullable ContentCaptureContext clientContext) {
-        mContext = context;
-        mHandler = handler;
-        mSystemServerInterface = systemServerInterface;
-        mDisabled = disabled;
+    protected ContentCaptureSession(@Nullable ContentCaptureContext clientContext) {
         mClientContext = clientContext;
         mCloseGuard.open("destroy");
     }
@@ -209,7 +114,7 @@
     /**
      * Gets the id used to identify this session.
      */
-    public ContentCaptureSessionId getContentCaptureSessionId() {
+    public final ContentCaptureSessionId getContentCaptureSessionId() {
         if (mContentCaptureSessionId == null) {
             mContentCaptureSessionId = new ContentCaptureSessionId(mId);
         }
@@ -217,37 +122,16 @@
     }
 
     /**
-     * Starts this session.
-     *
-     * @hide
-     */
-    void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
-        if (!isContentCaptureEnabled()) return;
-
-        if (VERBOSE) {
-            Log.v(TAG, "start(): token=" + applicationToken + ", comp="
-                    + ComponentName.flattenToShortString(activityComponent));
-        }
-
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleStartSession, this,
-                applicationToken, activityComponent));
-    }
-
-    /**
      * Flushes the buffered events to the service.
-     *
-     * @hide
      */
-    void flush() {
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleForceFlush, this));
-    }
+    abstract void flush();
 
     /**
      * Destroys this session, flushing out all pending notifications to the service.
      *
      * <p>Once destroyed, any new notification will be dropped.
      */
-    public void destroy() {
+    public final void destroy() {
         //TODO(b/111276913): mark it as destroyed so other methods are ignored (and test on CTS)
 
         if (!isContentCaptureEnabled()) return;
@@ -255,15 +139,19 @@
         //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);
+            Log.v(mTag, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
         }
 
         flush();
 
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleDestroySession, this));
+        onDestroy();
+
         mCloseGuard.close();
     }
 
+    abstract void onDestroy();
+
+
     /** @hide */
     @Override
     public void close() {
@@ -282,225 +170,6 @@
         }
     }
 
-    private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
-        if (mState != STATE_UNKNOWN) {
-            // TODO(b/111276913): revisit this scenario
-            Log.w(TAG, "ignoring handleStartSession(" + token + ") while on state "
-                    + getStateAsString(mState));
-            return;
-        }
-        mState = STATE_WAITING_FOR_SERVER;
-        mApplicationToken = token;
-        mComponentName = componentName;
-
-        if (VERBOSE) {
-            Log.v(TAG, "handleStartSession(): token=" + token + ", act="
-                    + getActivityDebugName() + ", id=" + mId);
-        }
-        final int flags = 0; // TODO(b/111276913): get proper flags
-
-        try {
-            mSystemServerInterface.startSession(mContext.getUserId(), mApplicationToken,
-                    componentName, mId, mClientContext, flags, new IResultReceiver.Stub() {
-                        @Override
-                        public void send(int resultCode, Bundle resultData) {
-                            IBinder binder = null;
-                            if (resultData != null) {
-                                binder = resultData.getBinder(EXTRA_BINDER);
-                                if (binder == null) {
-                                    Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
-                                    handleResetState();
-                                    return;
-                                }
-                            }
-                            handleSessionStarted(resultCode, binder);
-                        }
-                    });
-        } catch (RemoteException e) {
-            Log.w(TAG, "Error starting session for " + componentName.flattenToShortString() + ": "
-                    + e);
-        }
-    }
-
-    /**
-     * Callback from {@code system_server} after call to
-     * {@link IContentCaptureManager#startSession(int, IBinder, ComponentName, String,
-     * ContentCaptureContext, int, IResultReceiver)}.
-     *
-     * @param resultCode session state
-     * @param binder handle to {@link IContentCaptureDirectManager}
-     */
-    private void handleSessionStarted(int resultCode, @Nullable IBinder binder) {
-        mState = resultCode;
-        if (binder != null) {
-            mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
-            mDirectServiceVulture = () -> {
-                Log.w(TAG, "Destroying session " + mId + " because service died");
-                destroy();
-            };
-            try {
-                binder.linkToDeath(mDirectServiceVulture, 0);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
-            }
-        }
-        if (resultCode == STATE_DISABLED || resultCode == STATE_DISABLED_DUPLICATED_ID) {
-            mDisabled.set(true);
-            handleResetSession(/* resetState= */ false);
-        } else {
-            mDisabled.set(false);
-        }
-        if (VERBOSE) {
-            Log.v(TAG, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
-                    + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
-                    + ", binder=" + binder);
-        }
-    }
-
-    private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
-        if (mEvents == null) {
-            if (VERBOSE) {
-                Log.v(TAG, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
-            }
-            mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
-        }
-        mEvents.add(event);
-
-        final int numberEvents = mEvents.size();
-
-        // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
-        // buffered (either total or per autofillid). For
-        // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
-        // "a" and "b" then send "abc".
-        final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
-
-        if (bufferEvent && !forceFlush) {
-            handleScheduleFlush(/* checkExisting= */ true);
-            return;
-        }
-
-        if (mState != STATE_ACTIVE) {
-            // Callback from startSession hasn't been called yet - typically happens on system
-            // apps that are started before the system service
-            // TODO(b/111276913): try to ignore session while system is not ready / boot
-            // not complete instead. Similarly, the manager service should return right away
-            // when the user does not have a service set
-            if (VERBOSE) {
-                Log.v(TAG, "Closing session for " + getActivityDebugName()
-                        + " after " + numberEvents + " delayed events and state "
-                        + getStateAsString(mState));
-            }
-            handleResetState();
-            // TODO(b/111276913): blacklist activity / use special flag to indicate that
-            // when it's launched again
-            return;
-        }
-
-        handleForceFlush();
-    }
-
-    private void handleScheduleFlush(boolean checkExisting) {
-        if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
-            // "Renew" the flush message by removing the previous one
-            mHandler.removeMessages(MSG_FLUSH);
-        }
-        mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
-        if (VERBOSE) {
-            Log.v(TAG, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
-        }
-        mHandler.sendMessageDelayed(
-                obtainMessage(ContentCaptureSession::handleFlushIfNeeded, this).setWhat(MSG_FLUSH),
-                FLUSHING_FREQUENCY_MS);
-    }
-
-    private void handleFlushIfNeeded() {
-        if (mEvents.isEmpty()) {
-            if (VERBOSE) Log.v(TAG, "Nothing to flush");
-            return;
-        }
-        handleForceFlush();
-    }
-
-    private void handleForceFlush() {
-        if (mEvents == null) return;
-
-        if (mDirectServiceInterface == null) {
-            Log.w(TAG, "handleForceFlush(): client not available yet");
-            if (!mHandler.hasMessages(MSG_FLUSH)) {
-                handleScheduleFlush(/* checkExisting= */ false);
-            }
-            return;
-        }
-
-        final int numberEvents = mEvents.size();
-        try {
-            if (DEBUG) {
-                Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
-            }
-            mHandler.removeMessages(MSG_FLUSH);
-
-            final ParceledListSlice<ContentCaptureEvent> events = handleClearEvents();
-            mDirectServiceInterface.sendEvents(mId, events);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Error sending " + numberEvents + " for " + getActivityDebugName()
-                    + ": " + e);
-        }
-    }
-
-    /**
-     * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
-     */
-    @NonNull
-    private ParceledListSlice<ContentCaptureEvent> handleClearEvents() {
-        // NOTE: we must save a reference to the current mEvents and then set it to to null,
-        // otherwise clearing it would clear it in the receiving side if the service is also local.
-        final List<ContentCaptureEvent> events = mEvents == null
-                ? Collections.emptyList()
-                : mEvents;
-        mEvents = null;
-        return new ParceledListSlice<>(events);
-    }
-
-    private void handleDestroySession() {
-        //TODO(b/111276913): right now both the ContentEvents and lifecycle sessions are sent
-        // to system_server, so it's ok to call both in sequence here. But once we split
-        // them so the events are sent directly to the service, we need to make sure they're
-        // sent in order.
-        if (DEBUG) {
-            Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
-                    + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
-                    + getActivityDebugName());
-        }
-
-        try {
-            mSystemServerInterface.finishSession(mContext.getUserId(), mId);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error destroying system-service session " + mId + " for "
-                    + getActivityDebugName() + ": " + e);
-        }
-    }
-
-    private void handleResetState() {
-        handleResetSession(/* resetState= */ true);
-    }
-
-    // TODO(b/111276913): 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;
-        }
-        mContentCaptureSessionId = null;
-        mApplicationToken = null;
-        mComponentName = null;
-        mEvents = null;
-        if (mDirectServiceInterface != null) {
-            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
-        }
-        mDirectServiceInterface = null;
-        mHandler.removeMessages(MSG_FLUSH);
-    }
-
     /**
      * Notifies the Content Capture Service that a node has been added to the view structure.
      *
@@ -510,7 +179,7 @@
      *
      * @param node node that has been added.
      */
-    public void notifyViewAppeared(@NonNull ViewStructure node) {
+    public final void notifyViewAppeared(@NonNull ViewStructure node) {
         Preconditions.checkNotNull(node);
         if (!isContentCaptureEnabled()) return;
 
@@ -518,12 +187,11 @@
             throw new IllegalArgumentException("Invalid node class: " + node.getClass());
         }
 
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(TYPE_VIEW_APPEARED)
-                        .setViewNode(((ViewNode.ViewStructureImpl) node).mNode),
-                        /* forceFlush= */ false));
+        internalNotifyViewAppeared((ViewStructureImpl) node);
     }
 
+    abstract void internalNotifyViewAppeared(@NonNull ViewNode.ViewStructureImpl node);
+
     /**
      * Notifies the Content Capture Service that a node has been removed from the view structure.
      *
@@ -532,15 +200,15 @@
      *
      * @param id id of the node that has been removed.
      */
-    public void notifyViewDisappeared(@NonNull AutofillId id) {
+    public final void notifyViewDisappeared(@NonNull AutofillId id) {
         Preconditions.checkNotNull(id);
         if (!isContentCaptureEnabled()) return;
 
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
-                        /* forceFlush= */ false));
+        internalNotifyViewDisappeared(id);
     }
 
+    abstract void internalNotifyViewDisappeared(@NonNull AutofillId id);
+
     /**
      * Notifies the Intelligence Service that the value of a text node has been changed.
      *
@@ -549,24 +217,25 @@
      * @param flags either {@code 0} or {@link #FLAG_USER_INPUT} when the value was explicitly
      * changed by the user (for example, through the keyboard).
      */
-    public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+    public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
             int flags) {
         Preconditions.checkNotNull(id);
 
         if (!isContentCaptureEnabled()) return;
 
-        mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
-                new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
-                        .setText(text), /* forceFlush= */ false));
+        internalNotifyViewTextChanged(id, text, flags);
     }
 
+    abstract void internalNotifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+            int flags);
+
     /**
      * Creates a {@link ViewStructure} for a "standard" view.
      *
      * @hide
      */
     @NonNull
-    public ViewStructure newViewStructure(@NonNull View view) {
+    public final ViewStructure newViewStructure(@NonNull View view) {
         return new ViewNode.ViewStructureImpl(view);
     }
 
@@ -583,78 +252,25 @@
      * @hide
      */
     @NonNull
-    public ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, int virtualId) {
+    public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId,
+            int virtualId) {
         return new ViewNode.ViewStructureImpl(parentId, virtualId);
     }
 
-    private boolean isContentCaptureEnabled() {
-        return mSystemServerInterface != null && !mDisabled.get();
-    }
+    abstract boolean isContentCaptureEnabled();
 
-    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
-        pw.print(prefix); pw.print("id: "); pw.println(mId);
-        pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
-        pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
-        if (mSystemServerInterface != null) {
-            pw.print(prefix); pw.print("mSystemServerInterface: ");
-            pw.println(mSystemServerInterface);
-        }
-        if (mDirectServiceInterface != null) {
-            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) {
-            pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
-        }
-        pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
-        pw.print(getStateAsString(mState)); pw.println(")");
-        if (mApplicationToken != null) {
-            pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
-        }
-        if (mComponentName != null) {
-            pw.print(prefix); pw.print("component name: ");
-            pw.println(mComponentName.flattenToShortString());
-        }
-        if (mEvents != null && !mEvents.isEmpty()) {
-            final int numberEvents = mEvents.size();
-            pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
-            pw.print('/'); pw.println(MAX_BUFFER_SIZE);
-            if (VERBOSE && numberEvents > 0) {
-                final String prefix3 = prefix + "  ";
-                for (int i = 0; i < numberEvents; i++) {
-                    final ContentCaptureEvent event = mEvents.get(i);
-                    pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
-                    pw.println();
-                }
-            }
-            pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
-            pw.print(prefix); pw.print("next flush: ");
-            TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
-        }
-    }
-
-    /**
-     * Gets a string that can be used to identify the activity on logging statements.
-     */
-    private String getActivityDebugName() {
-        return mComponentName == null ? mContext.getPackageName()
-                : mComponentName.flattenToShortString();
-    }
+    abstract void dump(@NonNull String prefix, @NonNull PrintWriter pw);
 
     @Override
     public String toString() {
         return mId;
     }
 
+    /**
+     * @hide
+     */
     @NonNull
-    private static String getStateAsString(int state) {
+    protected static String getStateAsString(int state) {
         switch (state) {
             case STATE_UNKNOWN:
                 return "UNKNOWN";