Added new APIs to let Settings enable / disable ContentCapture

Bug: 123286662
Test: atest ChildlessActivityTest#testSetContentCaptureFeatureEnabled_disabledBySettings \
        ChildlessActivityTest#testSetContentCaptureFeatureEnabled_disabledThenReEnabledBySettings\
        FrameworksCoreTests:SettingsBackupTest#secureSettingsBackedUpOrBlacklisted
Test: atest CtsContentCaptureServiceTestCases # for sanity check

Change-Id: I7cd2c36c1d7e23efb9acacf4f18cecd8838f5ac5
diff --git a/api/system-current.txt b/api/system-current.txt
index 30d42a23..01e50c0 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5865,6 +5865,7 @@
     field public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH = "autofill_user_data_max_value_length";
     field public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH = "autofill_user_data_min_value_length";
     field public static final String COMPLETED_CATEGORY_PREFIX = "suggested.completed_category.";
+    field public static final String CONTENT_CAPTURE_ENABLED = "content_capture_enabled";
     field public static final String DOZE_ALWAYS_ON = "doze_always_on";
     field public static final String HUSH_GESTURE_USED = "hush_gesture_used";
     field public static final String INSTANT_APPS_ENABLED = "instant_apps_enabled";
@@ -9200,6 +9201,10 @@
     field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3
   }
 
+  public final class ContentCaptureManager {
+    method public boolean isContentCaptureFeatureEnabled();
+  }
+
   public final class UserDataRemovalRequest implements android.os.Parcelable {
     method @NonNull public String getPackageName();
     method @NonNull public java.util.List<android.view.contentcapture.UserDataRemovalRequest.UriRequest> getUriRequests();
diff --git a/api/test-current.txt b/api/test-current.txt
index 0f2ba12..406867b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1542,6 +1542,7 @@
     field public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE = "autofill_user_data_max_user_data_size";
     field public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH = "autofill_user_data_max_value_length";
     field public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH = "autofill_user_data_min_value_length";
+    field public static final String CONTENT_CAPTURE_ENABLED = "content_capture_enabled";
     field public static final String DISABLED_PRINT_SERVICES = "disabled_print_services";
     field @Deprecated public static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES = "enabled_notification_policy_access_packages";
     field public static final String LOCATION_ACCESS_CHECK_DELAY_MILLIS = "location_access_check_delay_millis";
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 852b65a..3f00793 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5656,6 +5656,14 @@
                 "autofill_user_data_min_value_length";
 
         /**
+         * Defines whether Content Capture is enabled  for the user.
+         * @hide
+         */
+        @SystemApi
+        @TestApi
+        public static final String CONTENT_CAPTURE_ENABLED = "content_capture_enabled";
+
+        /**
          * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead
          */
         @Deprecated
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 07c9101..07dedd6 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -19,6 +19,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UiThread;
 import android.content.ComponentName;
@@ -108,9 +109,7 @@
             if (mMainSession == null) {
                 mMainSession = new MainContentCaptureSession(mContext, mHandler, mService,
                         mDisabled);
-                if (VERBOSE) {
-                    Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
-                }
+                if (VERBOSE) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
             }
             return mMainSession;
         }
@@ -147,13 +146,9 @@
      */
     @Nullable
     public ComponentName getServiceComponentName() {
-        if (!isContentCaptureEnabled()) {
-            return null;
-        }
-        // Wait for system server to return the component name.
+        if (!isContentCaptureEnabled()) return null;
+
         final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
-
-
         try {
             mService.getServiceComponentName(resultReceiver);
             return resultReceiver.getParcelableResult();
@@ -164,6 +159,17 @@
 
     /**
      * Checks whether content capture is enabled for this activity.
+     *
+     * <p>There are many reasons it could be disabled, such as:
+     * <ul>
+     *   <li>App itself disabled content capture through {@link #setContentCaptureEnabled(boolean)}.
+     *   <li>Service disabled content capture for this specific activity.
+     *   <li>Service disabled content capture for all activities of this package.
+     *   <li>Service disabled content capture globally.
+     *   <li>User disabled content capture globally (through Settings).
+     *   <li>OEM disabled content capture globally.
+     *   <li>Transient errors.
+     * </ul>
      */
     public boolean isContentCaptureEnabled() {
         synchronized (mLock) {
@@ -184,6 +190,28 @@
     }
 
     /**
+     * Gets whether Content Capture is enabled for the given user.
+     *
+     * <p>This method is typically used by the Content Capture Service settings page, so it can
+     * provide a toggle to enable / disable it.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isContentCaptureFeatureEnabled() {
+        if (mService == null) return false;
+
+        final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+        try {
+            mService.isContentCaptureFeatureEnabled(resultReceiver);
+            return resultReceiver.getIntResult() == 1;
+        } catch (RemoteException e) {
+            // Unable to retrieve component name in a reasonable amount of time.
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Called by the app to request the Content Capture service to remove user-data associated with
      * some context.
      *
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index c425e7b..e6ee6ed 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -99,7 +99,8 @@
     public static final int STATE_FLAG_SECURE = 0x20;
 
     /**
-     * Session is disabled manually by the specific app.
+     * Session is disabled manually by the specific app
+     * (through {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}).
      *
      * @hide
      */
diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
index 56ed8bf..e3b0372 100644
--- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
@@ -62,4 +62,9 @@
      * Requests the removal of user data for the calling user.
      */
     void removeUserData(in UserDataRemovalRequest request);
+
+    /**
+     * Returns whether the content capture feature is enabled for the calling user.
+     */
+    void isContentCaptureFeatureEnabled(in IResultReceiver result);
 }
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index bd7f852..4e71821 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -605,6 +605,7 @@
                  Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG,
                  Settings.Secure.COMPLETED_CATEGORY_PREFIX,
                  Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS,
+                 Settings.Secure.CONTENT_CAPTURE_ENABLED,
                  Settings.Secure.DEFAULT_INPUT_METHOD,
                  Settings.Secure.DEVICE_PAIRED,
                  Settings.Secure.DIALER_DEFAULT_APPLICATION,
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 6108aaa..57c9d92 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -20,10 +20,14 @@
 import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -32,8 +36,10 @@
 import android.os.ShellCallback;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.util.LocalLog;
 import android.util.Slog;
+import android.util.SparseBooleanArray;
 import android.view.contentcapture.IContentCaptureManager;
 import android.view.contentcapture.UserDataRemovalRequest;
 
@@ -49,6 +55,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A service used to observe the contents of the screen.
@@ -60,23 +67,43 @@
 public final class ContentCaptureManagerService extends
         AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> {
 
-    private static final String TAG = ContentCaptureManagerService.class.getSimpleName();
-
     static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions";
 
     private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
 
-    @GuardedBy("mLock")
-    private ActivityManagerInternal mAm;
-
     private final LocalService mLocalService = new LocalService();
 
     private final LocalLog mRequestsHistory = new LocalLog(20);
 
+    @GuardedBy("mLock")
+    private ActivityManagerInternal mAm;
+
+    /**
+     * Users disabled by {@link android.provider.Settings.Secure#CONTENT_CAPTURE_ENABLED}.
+     */
+    @GuardedBy("mLock")
+    @Nullable
+    private SparseBooleanArray mDisabledUsers;
+
+
     public ContentCaptureManagerService(@NonNull Context context) {
         super(context, new FrameworkResourcesServiceNameResolver(context,
                 com.android.internal.R.string.config_defaultContentCaptureService),
                 UserManager.DISALLOW_CONTENT_CAPTURE);
+        // Sets which serviecs are disabled
+        final UserManager um = getContext().getSystemService(UserManager.class);
+        final List<UserInfo> users = um.getUsers();
+        for (int i = 0; i < users.size(); i++) {
+            final int userId = users.get(i).id;
+            final boolean disabled = isDisabledBySettings(userId);
+            if (disabled) {
+                Slog.i(mTag, "user " + userId + " disabled by settings");
+                if (mDisabledUsers == null) {
+                    mDisabledUsers = new SparseBooleanArray(1);
+                }
+                mDisabledUsers.put(userId, true);
+            }
+        }
     }
 
     @Override // from AbstractMasterSystemService
@@ -100,7 +127,7 @@
 
     @Override // from AbstractMasterSystemService
     protected void enforceCallingPermissionForManagement() {
-        getContext().enforceCallingPermission(MANAGE_CONTENT_CAPTURE, TAG);
+        getContext().enforceCallingPermission(MANAGE_CONTENT_CAPTURE, mTag);
     }
 
     @Override // from AbstractMasterSystemService
@@ -108,9 +135,86 @@
         return MAX_TEMP_SERVICE_DURATION_MS;
     }
 
+    @Override // from AbstractMasterSystemService
+    protected void registerForExtraSettingsChanges(@NonNull ContentResolver resolver,
+            @NonNull ContentObserver observer) {
+        resolver.registerContentObserver(Settings.Secure.getUriFor(
+                Settings.Secure.CONTENT_CAPTURE_ENABLED), false, observer,
+                UserHandle.USER_ALL);
+    }
+
+    @Override // from AbstractMasterSystemService
+    protected void onSettingsChanged(@UserIdInt int userId, @NonNull String property) {
+        switch (property) {
+            case Settings.Secure.CONTENT_CAPTURE_ENABLED:
+                setContentCaptureFeatureEnabledFromSettings(userId);
+                return;
+            default:
+                Slog.w(mTag, "Unexpected property (" + property + "); updating cache instead");
+        }
+    }
+
+    @Override // from AbstractMasterSystemService
+    protected boolean isDisabledLocked(@UserIdInt int userId) {
+        return isDisabledBySettingsLocked(userId) || super.isDisabledLocked(userId);
+    }
+
+    private boolean isDisabledBySettingsLocked(@UserIdInt int userId) {
+        return mDisabledUsers != null && mDisabledUsers.get(userId);
+    }
+
+    private void setContentCaptureFeatureEnabledFromSettings(@UserIdInt int userId) {
+        setContentCaptureFeatureEnabledForUser(userId, !isDisabledBySettings(userId));
+    }
+
+    private boolean isDisabledBySettings(@UserIdInt int userId) {
+        final String property = Settings.Secure.CONTENT_CAPTURE_ENABLED;
+        final String value = Settings.Secure.getStringForUser(getContext().getContentResolver(),
+                property, userId);
+        if (value == null) {
+            if (verbose) {
+                Slog.v(mTag, "isDisabledBySettings(): assuming false as '" + property
+                        + "' is not set");
+            }
+            return false;
+        }
+
+        try {
+            return !Boolean.valueOf(value);
+        } catch (Exception e) {
+            Slog.w(mTag, "Invalid value for property " + property + ": " + value);
+        }
+        return false;
+    }
+
+    private void setContentCaptureFeatureEnabledForUser(@UserIdInt int userId, boolean enabled) {
+        synchronized (mLock) {
+            if (mDisabledUsers == null) {
+                mDisabledUsers = new SparseBooleanArray();
+            }
+            final boolean alreadyEnabled = !mDisabledUsers.get(userId);
+            if (!(enabled ^ alreadyEnabled)) {
+                if (debug) {
+                    Slog.d(mTag, "setContentCaptureFeatureEnabledForUser(): already " + enabled);
+                }
+                return;
+            }
+            if (enabled) {
+                Slog.i(mTag, "setContentCaptureFeatureEnabled(): enabling service for user "
+                        + userId);
+                mDisabledUsers.delete(userId);
+            } else {
+                Slog.i(mTag, "setContentCaptureFeatureEnabled(): disabling service for user "
+                        + userId);
+                mDisabledUsers.put(userId, true);
+            }
+            updateCachedServiceLocked(userId, !enabled);
+        }
+    }
+
     // Called by Shell command.
     void destroySessions(@UserIdInt int userId, @NonNull IResultReceiver receiver) {
-        Slog.i(TAG, "destroySessions() for userId " + userId);
+        Slog.i(mTag, "destroySessions() for userId " + userId);
         enforceCallingPermissionForManagement();
 
         synchronized (mLock) {
@@ -133,7 +237,7 @@
 
     // Called by Shell command.
     void listSessions(int userId, IResultReceiver receiver) {
-        Slog.i(TAG, "listSessions() for userId " + userId);
+        Slog.i(mTag, "listSessions() for userId " + userId);
         enforceCallingPermissionForManagement();
 
         final Bundle resultData = new Bundle();
@@ -174,6 +278,13 @@
         return mAm;
     }
 
+    @Override // from AbstractMasterSystemService
+    protected void dumpLocked(String prefix, PrintWriter pw) {
+        super.dumpLocked(prefix, pw);
+
+        pw.print(prefix); pw.print("Disabled users: "); pw.println(mDisabledUsers);
+    }
+
     final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {
 
         @Override
@@ -222,8 +333,7 @@
                 result.send(/* resultCode= */ 0,
                         SyncResultReceiver.bundleFor(connectedServiceComponentName));
             } catch (RemoteException e) {
-                // Ignore exception as we need to be resilient against app behavior.
-                Slog.w(TAG, "Unable to send service component name: " + e);
+                Slog.w(mTag, "Unable to send service component name: " + e);
             }
         }
 
@@ -238,8 +348,22 @@
         }
 
         @Override
+        public void isContentCaptureFeatureEnabled(@NonNull IResultReceiver result) {
+            final int userId = UserHandle.getCallingUserId();
+            boolean enabled;
+            synchronized (mLock) {
+                enabled = !isDisabledBySettingsLocked(userId);
+            }
+            try {
+                result.send(enabled ? 1 : 0, /* resultData= */null);
+            } catch (RemoteException e) {
+                Slog.w(mTag, "Unable to send isContentCaptureFeatureEnabled(): " + e);
+            }
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
+            if (!DumpUtils.checkDumpPermission(getContext(), mTag, pw)) return;
 
             boolean showHistory = true;
             if (args != null) {
@@ -252,7 +376,7 @@
                             pw.println("Usage: dumpsys content_capture [--no-history]");
                             return;
                         default:
-                            Slog.w(TAG, "Ignoring invalid dump arg: " + arg);
+                            Slog.w(mTag, "Ignoring invalid dump arg: " + arg);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
index 8e26097..532aa01 100644
--- a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
+++ b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java
@@ -117,7 +117,7 @@
      */
     @GuardedBy("mLock")
     @Nullable
-    private final SparseBooleanArray mDisabledUsers;
+    private final SparseBooleanArray mDisabledByUserRestriction;
 
     /**
      * Cache of services per user id.
@@ -148,9 +148,9 @@
 
         }
         if (disallowProperty == null) {
-            mDisabledUsers = null;
+            mDisabledByUserRestriction = null;
         } else {
-            mDisabledUsers = new SparseBooleanArray();
+            mDisabledByUserRestriction = new SparseBooleanArray();
             // Hookup with UserManager to disable service when necessary.
             final UserManager um = context.getSystemService(UserManager.class);
             final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
@@ -159,15 +159,15 @@
                 final int userId = users.get(i).id;
                 final boolean disabled = umi.getUserRestriction(userId, disallowProperty);
                 if (disabled) {
-                    Slog.i(mTag, "Disabling for user " + userId);
-                    mDisabledUsers.put(userId, disabled);
+                    Slog.i(mTag, "Disabling by restrictions user " + userId);
+                    mDisabledByUserRestriction.put(userId, disabled);
                 }
             }
             umi.addUserRestrictionsListener((userId, newRestrictions, prevRestrictions) -> {
                 final boolean disabledNow =
                         newRestrictions.getBoolean(disallowProperty, false);
                 synchronized (mLock) {
-                    final boolean disabledBefore = mDisabledUsers.get(userId);
+                    final boolean disabledBefore = mDisabledByUserRestriction.get(userId);
                     if (disabledBefore == disabledNow) {
                         // Nothing changed, do nothing.
                         if (debug) {
@@ -176,7 +176,7 @@
                         }
                     }
                     Slog.i(mTag, "Updating for user " + userId + ": disabled=" + disabledNow);
-                    mDisabledUsers.put(userId, disabledNow);
+                    mDisabledByUserRestriction.put(userId, disabledNow);
                     updateCachedServiceLocked(userId, disabledNow);
                 }
             });
@@ -414,7 +414,7 @@
      * given user.
      */
     protected boolean isDisabledLocked(@UserIdInt int userId) {
-        return mDisabledUsers == null ? false : mDisabledUsers.get(userId);
+        return mDisabledByUserRestriction == null ? false : mDisabledByUserRestriction.get(userId);
     }
 
     /**
@@ -523,7 +523,8 @@
                     mServiceNameResolver.dumpShort(pw, userId); pw.println();
                 }
             }
-            pw.print(prefix); pw.print("Disabled users: "); pw.println(mDisabledUsers);
+            pw.print(prefix); pw.print("Users disabled by restriction: ");
+            pw.println(mDisabledByUserRestriction);
             pw.print(prefix); pw.print("Allow instant service: "); pw.println(mAllowInstantService);
             final String settingsProperty = getServiceSettingsProperty();
             if (settingsProperty != null) {