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) {