Optimized Content Capture workflow by caching some state at the application level.

Content Capture for an activity and/or package is only available when the Content Capture service
explicitly whitelists it. As the whitelist is kept at system-server level, it's better to fetch that
info when the application is started and cache it locally, so we can optimize the
ContentCaptureManager APIs to return quickly when it's disabled.

This CL also caches other values such as the buffer parameters.

Test: atest CtsContentCaptureServiceTestCases

Bug: 120494182
Bug: 121202151

Change-Id: I9d5211bca496ffa85ba9efc2a7bb32411834b787
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d29fedd..89e848b 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1110,7 +1110,7 @@
         super.attachBaseContext(newBase);
         if (newBase != null) {
             newBase.setAutofillClient(this);
-            newBase.setContentCaptureSupported(true);
+            newBase.setContentCaptureOptions(getContentCaptureOptions());
         }
     }
 
@@ -1120,12 +1120,6 @@
         return this;
     }
 
-    /** @hide */
-    @Override
-    public boolean isContentCaptureSupported() {
-        return true;
-    }
-
     /**
      * Register an {@link Application.ActivityLifecycleCallbacks} instance that receives
      * lifecycle callbacks for only this Activity.
@@ -7615,6 +7609,7 @@
         mWindow.setColorMode(info.colorMode);
 
         setAutofillCompatibilityEnabled(application.isAutofillCompatibilityEnabled());
+        setContentCaptureOptions(application.getContentCaptureOptions());
     }
 
     private void enableAutofillCompatibilityIfNeeded() {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 7908637..001cd69 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -46,6 +46,7 @@
 import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks2;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -746,6 +747,14 @@
 
         boolean autofillCompatibilityEnabled;
 
+        /**
+         * Content capture options for the application - when null, it means ContentCapture is not
+         * enabled for the package.
+         */
+        @Nullable
+        ContentCaptureOptions contentCaptureOptions;
+
+        @Override
         public String toString() {
             return "AppBindData{appInfo=" + appInfo + "}";
         }
@@ -966,7 +975,8 @@
                 boolean enableBinderTracking, boolean trackAllocation,
                 boolean isRestrictedBackupMode, boolean persistent, Configuration config,
                 CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
-                String buildSerial, boolean autofillCompatibilityEnabled) {
+                String buildSerial, boolean autofillCompatibilityEnabled,
+                ContentCaptureOptions contentCaptureOptions) {
 
             if (services != null) {
                 if (false) {
@@ -1014,6 +1024,7 @@
             data.initProfilerInfo = profilerInfo;
             data.buildSerial = buildSerial;
             data.autofillCompatibilityEnabled = autofillCompatibilityEnabled;
+            data.contentCaptureOptions = contentCaptureOptions;
             sendMessage(H.BIND_APPLICATION, data);
         }
 
@@ -6155,6 +6166,9 @@
             // Propagate autofill compat state
             app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);
 
+            // Propagate Content Capture options
+            app.setContentCaptureOptions(data.contentCaptureOptions);
+
             mInitialApplication = app;
 
             // don't bring up providers in restricted mode; they may depend on the
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 6908ca2..3a1e80d 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -23,6 +23,7 @@
 import android.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -217,7 +218,7 @@
     private AutofillClient mAutofillClient = null;
     private boolean mIsAutofillCompatEnabled;
 
-    private boolean mIsContentCaptureSupported = false;
+    private ContentCaptureOptions mContentCaptureOptions = null;
 
     private final Object mSync = new Object();
 
@@ -2388,14 +2389,14 @@
 
     /** @hide */
     @Override
-    public boolean isContentCaptureSupported() {
-        return mIsContentCaptureSupported;
+    public ContentCaptureOptions getContentCaptureOptions() {
+        return mContentCaptureOptions;
     }
 
     /** @hide */
     @Override
-    public void setContentCaptureSupported(boolean supported) {
-        mIsContentCaptureSupported = supported;
+    public void setContentCaptureOptions(ContentCaptureOptions options) {
+        mContentCaptureOptions = options;
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index e7a8c0e..b73092a 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -22,6 +22,7 @@
 import android.app.ResultInfo;
 import android.app.servertransaction.ClientTransaction;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -68,7 +69,8 @@
             int debugMode, boolean enableBinderTracking, boolean trackAllocation,
             boolean restrictedBackupMode, boolean persistent, in Configuration config,
             in CompatibilityInfo compatInfo, in Map services,
-            in Bundle coreSettings, in String buildSerial, boolean isAutofillCompatEnabled);
+            in Bundle coreSettings, in String buildSerial, boolean isAutofillCompatEnabled,
+            in ContentCaptureOptions contentCaptureOptions);
     void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
     void scheduleExit();
     void scheduleServiceArgs(IBinder token, in ParceledListSlice args);
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c12a92f..1faa2ac 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -41,6 +41,7 @@
 import android.companion.CompanionDeviceManager;
 import android.companion.ICompanionDeviceManager;
 import android.content.ClipboardManager;
+import android.content.ContentCaptureOptions;
 import android.content.Context;
 import android.content.IRestrictionsManager;
 import android.content.RestrictionsManager;
@@ -1125,16 +1126,19 @@
                     throws ServiceNotFoundException {
                 // Get the services without throwing as this is an optional feature
                 Context outerContext = ctx.getOuterContext();
-                if (outerContext.isContentCaptureSupported()) {
+                ContentCaptureOptions options = outerContext.getContentCaptureOptions();
+                // Options is null when the service didn't whitelist the activity or package
+                if (options != null) {
                     IBinder b = ServiceManager
                             .getService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
                     IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(b);
+                    // Service is null when not provided by OEM or disabled by kill-switch.
                     if (service != null) {
-                        // When feature is disabled, we return a null manager to apps so the
-                        // performance impact is practically zero
-                        return new ContentCaptureManager(outerContext, service);
+                        return new ContentCaptureManager(outerContext, service, options);
                     }
                 }
+                // When feature is disabled or app / package not whitelisted, we return a null
+                // manager to apps so the performance impact is practically zero
                 return null;
             }});
 
diff --git a/core/java/android/content/ContentCaptureOptions.aidl b/core/java/android/content/ContentCaptureOptions.aidl
new file mode 100644
index 0000000..82ffac42
--- /dev/null
+++ b/core/java/android/content/ContentCaptureOptions.aidl
@@ -0,0 +1,19 @@
+/*
+** Copyright 2019, 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.content;
+
+parcelable ContentCaptureOptions;
diff --git a/core/java/android/content/ContentCaptureOptions.java b/core/java/android/content/ContentCaptureOptions.java
new file mode 100644
index 0000000..2fe9f14
--- /dev/null
+++ b/core/java/android/content/ContentCaptureOptions.java
@@ -0,0 +1,176 @@
+/*
+ * 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.content;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.contentcapture.ContentCaptureManager;
+
+import java.io.PrintWriter;
+
+/**
+ * Content capture options for a given package.
+ *
+ * <p>This object is created by the Content Capture System Service and passed back to the app when
+ * the application is created.
+ *
+ * @hide
+ */
+@TestApi
+public final class ContentCaptureOptions implements Parcelable {
+
+    private static final String TAG = ContentCaptureOptions.class.getSimpleName();
+
+    /**
+     * Logging level for {@code logcat} statements.
+     */
+    public final int loggingLevel;
+
+    /**
+     * Maximum number of events that are buffered before sent to the app.
+     */
+    public final int maxBufferSize;
+
+    /**
+     * Frequency the buffer is flushed if idle.
+     */
+    public final int idleFlushingFrequencyMs;
+
+    /**
+     * Frequency the buffer is flushed if last event is a text change.
+     */
+    public final int textChangeFlushingFrequencyMs;
+
+    /**
+     * Size of events that are logging on {@code dump}.
+     */
+    public final int logHistorySize;
+
+    /**
+     * List of activities explicitly whitelisted for content capture (or {@code null} if whitelisted
+     * for all acitivites in the package).
+     */
+    @Nullable
+    public final ArraySet<ComponentName> whitelistedComponents;
+
+    public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
+            int textChangeFlushingFrequencyMs, int logHistorySize,
+            @Nullable ArraySet<ComponentName> whitelistedComponents) {
+        this.loggingLevel = loggingLevel;
+        this.maxBufferSize = maxBufferSize;
+        this.idleFlushingFrequencyMs = idleFlushingFrequencyMs;
+        this.textChangeFlushingFrequencyMs = textChangeFlushingFrequencyMs;
+        this.logHistorySize = logHistorySize;
+        this.whitelistedComponents = whitelistedComponents;
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public static ContentCaptureOptions forWhitelistingItself() {
+        final ActivityThread at = ActivityThread.currentActivityThread();
+        if (at == null) {
+            throw new IllegalStateException("No ActivityThread");
+        }
+
+        final String packageName = at.getApplication().getPackageName();
+
+        if (!"android.contentcaptureservice.cts".equals(packageName)) {
+            Log.e(TAG, "forWhitelistingItself(): called by " + packageName);
+            throw new SecurityException("Thou shall not pass!");
+        }
+
+        final ContentCaptureOptions options = new ContentCaptureOptions(
+                ContentCaptureManager.LOGGING_LEVEL_VERBOSE,
+                ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE,
+                ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS,
+                ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS,
+                ContentCaptureManager.DEFAULT_LOG_HISTORY_SIZE,
+                /* whitelistedComponents= */ null);
+        // Always log, as it's used by test only
+        Log.i(TAG, "forWhitelistingItself(" + packageName + "): " + options);
+
+        return options;
+    }
+
+    @Override
+    public String toString() {
+        return "ContentCaptureOptions [loggingLevel=" + loggingLevel + ", maxBufferSize="
+                + maxBufferSize + ", idleFlushingFrequencyMs=" + idleFlushingFrequencyMs
+                + ", textChangeFlushingFrequencyMs=" + textChangeFlushingFrequencyMs
+                + ", logHistorySize=" + logHistorySize + ", whitelistedComponents="
+                + whitelistedComponents + "]";
+    }
+
+    /** @hide */
+    public void dumpShort(@NonNull PrintWriter pw) {
+        pw.print("logLvl="); pw.print(loggingLevel);
+        pw.print(", bufferSize="); pw.print(maxBufferSize);
+        pw.print(", idle="); pw.print(idleFlushingFrequencyMs);
+        pw.print(", textIdle="); pw.print(textChangeFlushingFrequencyMs);
+        pw.print(", logSize="); pw.print(logHistorySize);
+        if (whitelistedComponents != null) {
+            pw.print(", whitelisted="); pw.print(whitelistedComponents);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(loggingLevel);
+        parcel.writeInt(maxBufferSize);
+        parcel.writeInt(idleFlushingFrequencyMs);
+        parcel.writeInt(textChangeFlushingFrequencyMs);
+        parcel.writeInt(logHistorySize);
+        parcel.writeArraySet(whitelistedComponents);
+    }
+
+    public static final Parcelable.Creator<ContentCaptureOptions> CREATOR =
+            new Parcelable.Creator<ContentCaptureOptions>() {
+
+                @Override
+                public ContentCaptureOptions createFromParcel(Parcel parcel) {
+                    final int loggingLevel = parcel.readInt();
+                    final int maxBufferSize = parcel.readInt();
+                    final int idleFlushingFrequencyMs = parcel.readInt();
+                    final int textChangeFlushingFrequencyMs = parcel.readInt();
+                    final int logHistorySize = parcel.readInt();
+                    @SuppressWarnings("unchecked")
+                    final ArraySet<ComponentName> whitelistedComponents =
+                            (ArraySet<ComponentName>) parcel.readArraySet(null);
+                    return new ContentCaptureOptions(loggingLevel, maxBufferSize,
+                            idleFlushingFrequencyMs, textChangeFlushingFrequencyMs, logHistorySize,
+                            whitelistedComponents);
+                }
+
+                @Override
+                public ContentCaptureOptions[] newArray(int size) {
+                    return new ContentCaptureOptions[size];
+                }
+
+    };
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 25bfba2..fdb0041 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5353,22 +5353,21 @@
     }
 
     /**
-     * Checks whether this context supports content capture.
+     * Gets the Content Capture options for this context, or {@code null} if it's not whitelisted.
      *
      * @hide
      */
-    // NOTE: for now we just need to check if it's supported so we can optimize calls that can be
-    // skipped when it isn't. Eventually, we might need a full
-    // ContentCaptureManager.ContentCaptureClient interface (as it's done with AutofillClient).
-    //
-    public boolean isContentCaptureSupported() {
-        return false;
+    @Nullable
+    public ContentCaptureOptions getContentCaptureOptions() {
+        return null;
     }
 
     /**
      * @hide
      */
-    public void setContentCaptureSupported(@SuppressWarnings("unused") boolean supported) {
+    @TestApi
+    public void setContentCaptureOptions(
+            @SuppressWarnings("unused") @Nullable ContentCaptureOptions options) {
     }
 
     /**
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 26ed3b7..68b4320 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -1044,7 +1044,7 @@
      */
     @TestApi
     @Override
-    public void setAutofillCompatibilityEnabled(boolean  autofillCompatEnabled) {
+    public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
         if (mBase != null) {
             mBase.setAutofillCompatibilityEnabled(autofillCompatEnabled);
         }
@@ -1054,15 +1054,18 @@
      * @hide
      */
     @Override
-    public boolean isContentCaptureSupported() {
-        return mBase.isContentCaptureSupported();
+    public ContentCaptureOptions getContentCaptureOptions() {
+        return mBase == null ? null : mBase.getContentCaptureOptions();
     }
 
     /**
      * @hide
      */
+    @TestApi
     @Override
-    public void setContentCaptureSupported(boolean supported) {
-        mBase.setContentCaptureSupported(supported);
+    public void setContentCaptureOptions(ContentCaptureOptions options) {
+        if (mBase != null) {
+            mBase.setContentCaptureOptions(options);
+        }
     }
 }
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index d361a2c..d032e56 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -15,6 +15,9 @@
  */
 package android.service.contentcapture;
 
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
 import android.annotation.CallSuper;
@@ -66,10 +69,6 @@
 
     private static final String TAG = ContentCaptureService.class.getSimpleName();
 
-    // TODO(b/121044306): STOPSHIP use dynamic value, or change to false
-    static final boolean DEBUG = true;
-    static final boolean VERBOSE = false;
-
     /**
      * The {@link Intent} that must be declared as handled by the service.
      *
@@ -89,7 +88,9 @@
     private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
 
         @Override
-        public void onConnected(IBinder callback) {
+        public void onConnected(IBinder callback, boolean verbose, boolean debug) {
+            sVerbose = verbose;
+            sDebug = debug;
             mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected,
                     ContentCaptureService.this, callback));
         }
@@ -227,7 +228,7 @@
      */
     public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
             @NonNull ContentCaptureSessionId sessionId) {
-        if (VERBOSE) {
+        if (sVerbose) {
             Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
         }
     }
@@ -240,7 +241,7 @@
     @Deprecated
     public void onContentCaptureEventsRequest(@NonNull ContentCaptureSessionId sessionId,
             @NonNull ContentCaptureEventsRequest request) {
-        if (VERBOSE) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
+        if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
     }
 
     /**
@@ -252,7 +253,7 @@
      */
     public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
             @NonNull ContentCaptureEvent event) {
-        if (VERBOSE) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
+        if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
         onContentCaptureEventsRequest(sessionId, new ContentCaptureEventsRequest(event));
     }
 
@@ -262,7 +263,7 @@
      * @param request the user data requested to be removed
      */
     public void onUserDataRemovalRequest(@NonNull UserDataRemovalRequest request) {
-        if (VERBOSE) Log.v(TAG, "onUserDataRemovalRequest()");
+        if (sVerbose) Log.v(TAG, "onUserDataRemovalRequest()");
     }
 
     /**
@@ -280,14 +281,14 @@
      * @param sessionId the id of the session to destroy
      * */
     public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
-        if (VERBOSE) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
+        if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
     }
 
     /**
      * Disables the Content Capture service for the given user.
      */
     public final void disableContentCaptureServices() {
-        if (DEBUG) Log.d(TAG, "disableContentCaptureServices()");
+        if (sDebug) Log.d(TAG, "disableContentCaptureServices()");
 
         final IContentCaptureServiceCallback callback = mCallback;
         if (callback == null) {
@@ -313,6 +314,7 @@
     @Override
     @CallSuper
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
         final int size = mSessionUids.size();
         pw.print("Number sessions: "); pw.println(size);
         if (size > 0) {
@@ -422,7 +424,7 @@
         }
         final Integer rightUid = mSessionUids.get(sessionId);
         if (rightUid == null) {
-            if (VERBOSE) {
+            if (sVerbose) {
                 Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
                         + ": " + mSessionUids);
             }
diff --git a/core/java/android/service/contentcapture/IContentCaptureService.aidl b/core/java/android/service/contentcapture/IContentCaptureService.aidl
index d92fb3b..eb65032 100644
--- a/core/java/android/service/contentcapture/IContentCaptureService.aidl
+++ b/core/java/android/service/contentcapture/IContentCaptureService.aidl
@@ -31,7 +31,7 @@
  * @hide
  */
 oneway interface IContentCaptureService {
-    void onConnected(IBinder callback);
+    void onConnected(IBinder callback, boolean verbose, boolean debug);
     void onDisconnected();
     void onSessionStarted(in ContentCaptureContext context, String sessionId, int uid,
                           in IResultReceiver clientReceiver);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a78f2b0..278b9ff 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -9378,7 +9378,7 @@
         AttachInfo ai = mAttachInfo;
 
         // First check if context has client, so it saves a service lookup when it doesn't
-        if (!mContext.isContentCaptureSupported()) return;
+        if (mContext.getContentCaptureOptions() == null) return;
 
         // Then check if it's enabled in the context...
         final ContentCaptureManager ccm = ai != null ? ai.getContentCaptureManager(mContext)
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b1fee2d..ab4847d 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3497,7 +3497,7 @@
         }
         try {
             // First check if context supports it, so it saves a service lookup when it doesn't
-            if (!mContext.isContentCaptureSupported()) return;
+            if (mContext.getContentCaptureOptions() == null) return;
 
             // Then check if it's enabled in the contex itself.
             final ContentCaptureManager ccm = mContext
diff --git a/core/java/android/view/contentcapture/ContentCaptureHelper.java b/core/java/android/view/contentcapture/ContentCaptureHelper.java
index 1cf27fc..6e84ff0 100644
--- a/core/java/android/view/contentcapture/ContentCaptureHelper.java
+++ b/core/java/android/view/contentcapture/ContentCaptureHelper.java
@@ -63,12 +63,27 @@
     }
 
     /**
+     * Gets the default logging level for the device.
+     */
+    @LoggingLevel
+    public static int getDefaultLoggingLevel() {
+        return Build.IS_DEBUGGABLE ? LOGGING_LEVEL_DEBUG : LOGGING_LEVEL_OFF;
+    }
+
+    /**
      * Sets the value of the static logging level constants based on device config.
      */
     public static void setLoggingLevel() {
-        final int defaultLevel = Build.IS_DEBUGGABLE ? LOGGING_LEVEL_DEBUG : LOGGING_LEVEL_OFF;
+        final int defaultLevel = getDefaultLoggingLevel();
         final int level = getIntDeviceConfigProperty(DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
                 defaultLevel);
+        setLoggingLevel(level);
+    }
+
+    /**
+     * Sets the value of the static logging level constants based the given level.
+     */
+    public static void setLoggingLevel(@LoggingLevel int level) {
         Log.i(TAG, "Setting logging level to " + getLoggingLevelAsString(level));
         sVerbose = sDebug = false;
         switch (level) {
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 87e358c..336d997 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -26,6 +26,7 @@
 import android.annotation.TestApi;
 import android.annotation.UiThread;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.Context;
 import android.os.Handler;
 import android.os.IBinder;
@@ -150,6 +151,16 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface LoggingLevel {}
 
+
+    /** @hide */
+    public static final int DEFAULT_MAX_BUFFER_SIZE = 100;
+    /** @hide */
+    public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000;
+    /** @hide */
+    public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000;
+    /** @hide */
+    public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
+
     private final Object mLock = new Object();
 
     @NonNull
@@ -158,6 +169,9 @@
     @NonNull
     private final IContentCaptureManager mService;
 
+    @NonNull
+    final ContentCaptureOptions mOptions;
+
     // Flags used for starting session.
     @GuardedBy("mLock")
     private int mFlags;
@@ -172,14 +186,12 @@
 
     /** @hide */
     public ContentCaptureManager(@NonNull Context context,
-            @NonNull IContentCaptureManager service) {
+            @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
         mContext = Preconditions.checkNotNull(context, "context cannot be null");
         mService = Preconditions.checkNotNull(service, "service cannot be null");
+        mOptions = Preconditions.checkNotNull(options, "options cannot be null");
 
-        // TODO(b/123096662): right now we're reading the device config values here, but ideally
-        // it should be read on ContentCaptureManagerService and passed back when the activity
-        // started.
-        ContentCaptureHelper.setLoggingLevel();
+        ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel);
 
         if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
 
@@ -355,12 +367,13 @@
         synchronized (mLock) {
             pw.print(prefix2); pw.print("isContentCaptureEnabled(): ");
             pw.println(isContentCaptureEnabled());
-            pw.print(prefix); pw.print("Debug: "); pw.print(sDebug);
+            pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug);
             pw.print(" Verbose: "); pw.println(sVerbose);
-            pw.print(prefix); pw.print("Context: "); pw.println(mContext);
-            pw.print(prefix); pw.print("User: "); pw.println(mContext.getUserId());
-            pw.print(prefix); pw.print("Service: "); pw.println(mService);
-            pw.print(prefix); pw.print("Flags: "); pw.println(mFlags);
+            pw.print(prefix2); pw.print("Context: "); pw.println(mContext);
+            pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId());
+            pw.print(prefix2); pw.print("Service: "); pw.println(mService);
+            pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags);
+            pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println();
             if (mMainSession != null) {
                 final String prefix3 = prefix2 + "  ";
                 pw.print(prefix2); pw.println("Main session:");
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index f4021b1..0abf689 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -23,13 +23,9 @@
 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.ContentCaptureHelper.getIntDeviceConfigProperty;
 import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -76,10 +72,6 @@
      */
     private static final int MSG_FLUSH = 1;
 
-    private static final int DEFAULT_MAX_BUFFER_SIZE = 100;
-    private static final int DEFAULT_FLUSHING_FREQUENCY_MS = 5_000;
-    private static final int DEFAULT_LOG_HISTORY_SIZE = 10;
-
     /**
      * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
      * @hide
@@ -128,16 +120,6 @@
     @Nullable
     private ArrayList<ContentCaptureEvent> mEvents;
 
-    /**
-     * Maximum number of events that are buffered before sent to the app.
-     */
-    private final int mMaxBufferSize;
-
-    /**
-     * Frequency the buffer is flushed if idle.
-     */
-    private final int mIdleFlushingFrequencyMs;
-
     // Used just for debugging purposes (on dump)
     private long mNextFlush;
 
@@ -153,16 +135,7 @@
         mHandler = handler;
         mSystemServerInterface = systemServerInterface;
 
-        // TODO(b/123096662): right now we're reading the device config values here, but ideally
-        // it should be read on ContentCaptureManagerService and passed back when the activity
-        // started.
-        mMaxBufferSize = getIntDeviceConfigProperty(DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
-                DEFAULT_MAX_BUFFER_SIZE);
-        mIdleFlushingFrequencyMs = getIntDeviceConfigProperty(
-                DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY, DEFAULT_FLUSHING_FREQUENCY_MS);
-        final int logHistorySize = getIntDeviceConfigProperty(
-                DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, DEFAULT_LOG_HISTORY_SIZE);
-
+        final int logHistorySize = mManager.mOptions.logHistorySize;
         mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
     }
 
@@ -302,11 +275,12 @@
             if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
             return;
         }
+        final int maxBufferSize = mManager.mOptions.maxBufferSize;
         if (mEvents == null) {
             if (sVerbose) {
-                Log.v(TAG, "handleSendEvent(): creating buffer for " + mMaxBufferSize + " events");
+                Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
             }
-            mEvents = new ArrayList<>(mMaxBufferSize);
+            mEvents = new ArrayList<>(maxBufferSize);
         }
 
         // Some type of events can be merged together
@@ -347,14 +321,14 @@
 
         final int numberEvents = mEvents.size();
 
-        final boolean bufferEvent = numberEvents < mMaxBufferSize;
+        final boolean bufferEvent = numberEvents < maxBufferSize;
 
         if (bufferEvent && !forceFlush) {
             scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
             return;
         }
 
-        if (mState != STATE_ACTIVE && numberEvents >= mMaxBufferSize) {
+        if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
             // Callback from startSession hasn't been called yet - typically happens on system
             // apps that are started before the system service
             // TODO(b/122959591): try to ignore session while system is not ready / boot
@@ -435,13 +409,14 @@
             // "Renew" the flush message by removing the previous one
             mHandler.removeMessages(MSG_FLUSH);
         }
-        mNextFlush = System.currentTimeMillis() + mIdleFlushingFrequencyMs;
+        final int idleFlushingFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
+        mNextFlush = System.currentTimeMillis() + idleFlushingFrequencyMs;
         if (sVerbose) {
             Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
-                    + mIdleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
+                    + idleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
         }
         // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
-        mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, mIdleFlushingFrequencyMs);
+        mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, idleFlushingFrequencyMs);
     }
 
     @UiThread
@@ -483,7 +458,8 @@
         if (mFlushHistory != null) {
             // Logs reason, size, max size, idle timeout
             final String logRecord = "r=" + reasonString + " s=" + numberEvents
-                    + " m=" + mMaxBufferSize + " i=" + mIdleFlushingFrequencyMs;
+                    + " m=" + mManager.mOptions.maxBufferSize
+                    + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
             mFlushHistory.log(logRecord);
         }
         try {
@@ -649,7 +625,6 @@
 
         pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
         pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
-        pw.print(prefix); pw.print("mSystemServerInterface: ");
         if (mDirectServiceInterface != null) {
             pw.print(prefix); pw.print("mDirectServiceInterface: ");
             pw.println(mDirectServiceInterface);
@@ -667,7 +642,7 @@
         if (mEvents != null && !mEvents.isEmpty()) {
             final int numberEvents = mEvents.size();
             pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
-            pw.print('/'); pw.println(mMaxBufferSize);
+            pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
             if (sVerbose && numberEvents > 0) {
                 final String prefix3 = prefix + "  ";
                 for (int i = 0; i < numberEvents; i++) {
@@ -676,7 +651,8 @@
                     pw.println();
                 }
             }
-            pw.print(prefix); pw.print("flush frequency: "); pw.println(mIdleFlushingFrequencyMs);
+            pw.print(prefix); pw.print("flush frequency: ");
+            pw.println(mManager.mOptions.idleFlushingFrequencyMs);
             pw.print(prefix); pw.print("next flush: ");
             TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
             pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
diff --git a/core/java/com/android/internal/policy/DecorContext.java b/core/java/com/android/internal/policy/DecorContext.java
index 429c618..67cdd5d 100644
--- a/core/java/com/android/internal/policy/DecorContext.java
+++ b/core/java/com/android/internal/policy/DecorContext.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.policy;
 
+import android.content.ContentCaptureOptions;
 import android.content.Context;
 import android.content.res.AssetManager;
 import android.content.res.Resources;
@@ -93,7 +94,11 @@
     }
 
     @Override
-    public boolean isContentCaptureSupported() {
-        return true;
+    public ContentCaptureOptions getContentCaptureOptions() {
+        Context activityContext = mActivityContext.get();
+        if (activityContext != null) {
+            return activityContext.getContentCaptureOptions();
+        }
+        return null;
     }
 }
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index a5ea441..9fabe44 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -29,6 +29,7 @@
 import android.app.IUiAutomationConnection;
 import android.app.ProfilerInfo;
 import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -406,7 +407,7 @@
                 IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
                 boolean b2, boolean b3, Configuration configuration,
                 CompatibilityInfo compatibilityInfo, Map map, Bundle bundle1, String s1,
-                boolean autofillCompatEnabled) throws RemoteException {
+                boolean autofillCompatEnabled, ContentCaptureOptions o) throws RemoteException {
         }
 
         @Override
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
index 312e0e0..cd885e0 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java
@@ -41,7 +41,8 @@
 
     @Before
     public void before() {
-        mManager = new ContentCaptureManager(mMockContext, null);
+        mManager = new ContentCaptureManager(mMockContext, /* service= */ null,
+                /* options= */ null);
     }
 
     @Test