Fixed content capture whitelist for specific activities.
Test: manual verification (it cannot be fully verified using the current CTS setup)
Test: atest CtsContentCaptureServiceTestCases:android.contentcaptureservice.cts.WhitelistTest
Test: atest FrameworksCoreTests:android.content.ContentCaptureOptionsTest
Test: atest CtsContentCaptureServiceTestCases # sanity check
Test: m update-api
Fixes: 130573023
Change-Id: I2c76a01bd98c4154c4c59099f1368232d2dba80d
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 0d22f3a..c8bd275 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -127,6 +127,7 @@
import android.view.autofill.IAutofillWindowPresenter;
import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
import android.widget.AdapterView;
import android.widget.Toast;
import android.widget.Toolbar;
@@ -723,7 +724,7 @@
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
- AutofillManager.AutofillClient {
+ AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
private static final String TAG = "Activity";
private static final boolean DEBUG_LIFECYCLE = false;
@@ -1125,6 +1126,12 @@
return this;
}
+ /** @hide */
+ @Override
+ public final ContentCaptureClient getContentCaptureClient() {
+ return this;
+ }
+
/**
* Register an {@link Application.ActivityLifecycleCallbacks} instance that receives
* lifecycle callbacks for only this Activity.
@@ -6511,6 +6518,12 @@
return getComponentName();
}
+ /** @hide */
+ @Override
+ public final ComponentName contentCaptureClientGetComponentName() {
+ return getComponentName();
+ }
+
/**
* Retrieve a {@link SharedPreferences} object for accessing preferences
* that are private to this activity. This simply calls the underlying
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 98b658d..c48c878 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -1143,7 +1143,7 @@
Context outerContext = ctx.getOuterContext();
ContentCaptureOptions options = outerContext.getContentCaptureOptions();
// Options is null when the service didn't whitelist the activity or package
- if (options != null) {
+ if (options != null && (options.lite || options.isWhitelisted(outerContext))) {
IBinder b = ServiceManager
.getService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(b);
diff --git a/core/java/android/content/ContentCaptureOptions.java b/core/java/android/content/ContentCaptureOptions.java
index 76c4fb8..cb2142c 100644
--- a/core/java/android/content/ContentCaptureOptions.java
+++ b/core/java/android/content/ContentCaptureOptions.java
@@ -24,6 +24,9 @@
import android.util.ArraySet;
import android.util.Log;
import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
@@ -78,12 +81,19 @@
*/
public final boolean lite;
+ /**
+ * Constructor for "lite" objects that are just used to enable a {@link ContentCaptureManager}
+ * for contexts belonging to the content capture service app.
+ */
public ContentCaptureOptions(int loggingLevel) {
this(/* lite= */ true, loggingLevel, /* maxBufferSize= */ 0,
/* idleFlushingFrequencyMs= */ 0, /* textChangeFlushingFrequencyMs= */ 0,
/* logHistorySize= */ 0, /* whitelistedComponents= */ null);
}
+ /**
+ * Default constructor.
+ */
public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
int textChangeFlushingFrequencyMs, int logHistorySize,
@Nullable ArraySet<ComponentName> whitelistedComponents) {
@@ -91,6 +101,16 @@
textChangeFlushingFrequencyMs, logHistorySize, whitelistedComponents);
}
+ /** @hide */
+ @VisibleForTesting
+ public ContentCaptureOptions(@Nullable ArraySet<ComponentName> whitelistedComponents) {
+ this(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);
+ }
+
private ContentCaptureOptions(boolean lite, int loggingLevel, int maxBufferSize,
int idleFlushingFrequencyMs, int textChangeFlushingFrequencyMs, int logHistorySize,
@Nullable ArraySet<ComponentName> whitelistedComponents) {
@@ -103,10 +123,6 @@
this.whitelistedComponents = whitelistedComponents;
}
- /**
- * @hide
- */
- @TestApi
public static ContentCaptureOptions forWhitelistingItself() {
final ActivityThread at = ActivityThread.currentActivityThread();
if (at == null) {
@@ -120,19 +136,27 @@
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);
+ final ContentCaptureOptions options =
+ new ContentCaptureOptions(/* whitelistedComponents= */ null);
// Always log, as it's used by test only
Log.i(TAG, "forWhitelistingItself(" + packageName + "): " + options);
return options;
}
+ /** @hide */
+ @VisibleForTesting
+ public boolean isWhitelisted(@NonNull Context context) {
+ if (whitelistedComponents == null) return true; // whole package is whitelisted
+ final ContentCaptureClient client = context.getContentCaptureClient();
+ if (client == null) {
+ // Shouldn't happen, but it doesn't hurt to check...
+ Log.w(TAG, "isWhitelisted(): no ContentCaptureClient on " + context);
+ return false;
+ }
+ return whitelistedComponents.contains(client.contentCaptureClientGetComponentName());
+ }
+
@Override
public String toString() {
if (lite) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index de04829..00238bf 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -70,6 +70,7 @@
import android.view.ViewDebug;
import android.view.WindowManager;
import android.view.autofill.AutofillManager.AutofillClient;
+import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
import android.view.textclassifier.TextClassificationManager;
import java.io.File;
@@ -5414,6 +5415,14 @@
/**
* @hide
*/
+ @Nullable
+ public ContentCaptureClient getContentCaptureClient() {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
public final boolean isAutofillCompatibilityEnabled() {
final AutofillOptions options = getAutofillOptions();
return options != null && options.compatModeEnabled;
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 1c8288e..2539356 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -343,6 +343,15 @@
private MainContentCaptureSession mMainSession;
/** @hide */
+ public interface ContentCaptureClient {
+ /**
+ * Gets the component name of the client.
+ */
+ @NonNull
+ ComponentName contentCaptureClientGetComponentName();
+ }
+
+ /** @hide */
public ContentCaptureManager(@NonNull Context context,
@NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
mContext = Preconditions.checkNotNull(context, "context cannot be null");
diff --git a/core/java/com/android/internal/infra/GlobalWhitelistState.java b/core/java/com/android/internal/infra/GlobalWhitelistState.java
index dfa59b7..a0b2f94 100644
--- a/core/java/com/android/internal/infra/GlobalWhitelistState.java
+++ b/core/java/com/android/internal/infra/GlobalWhitelistState.java
@@ -35,11 +35,13 @@
*
* <p>This class is thread safe.
*/
+// TODO: add unit tests
public class GlobalWhitelistState {
// Uses full-name to avoid collision with service-provided mLock
protected final Object mGlobalWhitelistStateLock = new Object();
+ // TODO: should not be exposed directly
@Nullable
@GuardedBy("mGlobalWhitelistStateLock")
protected SparseArray<WhitelistHelper> mWhitelisterHelpers;
diff --git a/core/java/com/android/internal/infra/WhitelistHelper.java b/core/java/com/android/internal/infra/WhitelistHelper.java
index d7753db..9d653ba 100644
--- a/core/java/com/android/internal/infra/WhitelistHelper.java
+++ b/core/java/com/android/internal/infra/WhitelistHelper.java
@@ -98,9 +98,9 @@
@Nullable List<ComponentName> components) {
final ArraySet<String> packageNamesSet = packageNames == null ? null
: new ArraySet<>(packageNames);
- final ArraySet<ComponentName> componentssSet = components == null ? null
+ final ArraySet<ComponentName> componentsSet = components == null ? null
: new ArraySet<>(components);
- setWhitelist(packageNamesSet, componentssSet);
+ setWhitelist(packageNamesSet, componentsSet);
}
/**
@@ -170,7 +170,7 @@
pw.print("["); pw.print(components.valueAt(0));
for (int j = 1; j < components.size(); j++) {
- pw.print(", "); pw.print(components.valueAt(i));
+ pw.print(", "); pw.print(components.valueAt(j));
}
pw.println("]");
}
diff --git a/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
new file mode 100644
index 0000000..c6f4fa2
--- /dev/null
+++ b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArraySet;
+import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * Unit test for {@link ContentCaptureOptions}.
+ *
+ * <p>To run it:
+ * {@code atest FrameworksCoreTests:android.content.ContentCaptureOptionsTest}
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ContentCaptureOptionsTest {
+
+ private final ComponentName mContextComponent = new ComponentName("marco", "polo");
+ private final ComponentName mComp1 = new ComponentName("comp", "one");
+ private final ComponentName mComp2 = new ComponentName("two", "comp");
+
+ @Mock private Context mContext;
+ @Mock private ContentCaptureClient mClient;
+
+ @Before
+ public void setExpectation() {
+ when(mClient.contentCaptureClientGetComponentName()).thenReturn(mContextComponent);
+ when(mContext.getContentCaptureClient()).thenReturn(mClient);
+ }
+
+ @Test
+ public void testIsWhitelisted_nullWhitelistedComponents() {
+ ContentCaptureOptions options = new ContentCaptureOptions(null);
+ assertThat(options.isWhitelisted(mContext)).isTrue();
+ }
+
+ @Test
+ public void testIsWhitelisted_emptyWhitelistedComponents() {
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet((ComponentName) null));
+ assertThat(options.isWhitelisted(mContext)).isFalse();
+ }
+
+ @Test
+ public void testIsWhitelisted_notWhitelisted() {
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mComp2));
+ assertThat(options.isWhitelisted(mContext)).isFalse();
+ }
+
+ @Test
+ public void testIsWhitelisted_whitelisted() {
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mContextComponent));
+ assertThat(options.isWhitelisted(mContext)).isTrue();
+ }
+
+ @Test
+ public void testIsWhitelisted_invalidContext() {
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+ Context invalidContext = mock(Context.class); // has no client
+ assertThat(options.isWhitelisted(invalidContext)).isFalse();
+ }
+
+ @Test
+ public void testIsWhitelisted_clientWithNullComponentName() {
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+ ContentCaptureClient client = mock(ContentCaptureClient.class);
+ Context context = mock(Context.class);
+ when(context.getContentCaptureClient()).thenReturn(client);
+
+ assertThat(options.isWhitelisted(context)).isFalse();
+ }
+
+ @NonNull
+ private ArraySet<ComponentName> toSet(@Nullable ComponentName... comps) {
+ ArraySet<ComponentName> set = new ArraySet<>();
+ if (comps != null) {
+ for (int i = 0; i < comps.length; i++) {
+ set.add(comps[i]);
+ }
+ }
+ return set;
+ }
+}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 757c2dc..94e617d 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -781,36 +781,46 @@
@GuardedBy("mGlobalWhitelistStateLock")
public ContentCaptureOptions getOptions(@UserIdInt int userId,
@NonNull String packageName) {
+ boolean packageWhitelisted;
+ ArraySet<ComponentName> whitelistedComponents = null;
synchronized (mGlobalWhitelistStateLock) {
- if (!isWhitelisted(userId, packageName)) {
- if (packageName.equals(mServicePackages.get(userId))) {
+ packageWhitelisted = isWhitelisted(userId, packageName);
+ if (!packageWhitelisted) {
+ // Full package is not whitelisted: check individual components first
+ whitelistedComponents = getWhitelistedComponents(userId, packageName);
+ if (whitelistedComponents == null
+ && packageName.equals(mServicePackages.get(userId))) {
+ // No components whitelisted either, but let it go because it's the
+ // service's own package
if (verbose) Slog.v(mTag, "getOptionsForPackage() lite for " + packageName);
return new ContentCaptureOptions(mDevCfgLoggingLevel);
}
- if (verbose) {
- Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted");
- }
+ }
+ } // synchronized
+
+ // Restrict what temporary services can whitelist
+ if (Build.IS_USER && mServiceNameResolver.isTemporary(userId)) {
+ if (!packageName.equals(mServicePackages.get(userId))) {
+ Slog.w(mTag, "Ignoring package " + packageName + " while using temporary "
+ + "service " + mServicePackages.get(userId));
return null;
}
-
- final ArraySet<ComponentName> whitelistedComponents =
- getWhitelistedComponents(userId, packageName);
- if (Build.IS_USER && mServiceNameResolver.isTemporary(userId)) {
- if (!packageName.equals(mServicePackages.get(userId))) {
- Slog.w(mTag, "Ignoring package " + packageName
- + " while using temporary service " + mServicePackages.get(userId));
- return null;
- }
- }
- final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
- mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
- mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
- whitelistedComponents);
- if (verbose) {
- Slog.v(mTag, "getOptionsForPackage(" + packageName + "): " + options);
- }
- return options;
}
+
+ if (!packageWhitelisted && whitelistedComponents == null) {
+ // No can do!
+ if (verbose) {
+ Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted");
+ }
+ return null;
+ }
+
+ final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
+ mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
+ mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
+ whitelistedComponents);
+ if (verbose) Slog.v(mTag, "getOptionsForPackage(" + packageName + "): " + options);
+ return options;
}
@Override
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 4f14b13..35d3452 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -35,13 +35,11 @@
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.content.ComponentName;
-import android.content.ContentCaptureOptions;
import android.content.pm.ActivityPresentationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.UserHandle;
@@ -60,7 +58,6 @@
import android.view.contentcapture.UserDataRemovalRequest;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.infra.WhitelistHelper;
import com.android.internal.os.IResultReceiver;
import com.android.server.LocalServices;
import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks;
@@ -97,12 +94,6 @@
new ContentCaptureServiceRemoteCallback();
/**
- * List of packages that are whitelisted to be content captured.
- */
- @GuardedBy("mLock")
- private final WhitelistHelper mWhitelistHelper = new WhitelistHelper();
-
- /**
* List of conditions keyed by package.
*/
@GuardedBy("mLock")
@@ -131,6 +122,7 @@
* Updates the reference to the remote service.
*/
private void updateRemoteServiceLocked(boolean disabled) {
+ if (mMaster.verbose) Slog.v(TAG, "updateRemoteService(disabled=" + disabled + ")");
if (mRemoteService != null) {
if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service");
mRemoteService.destroy();
@@ -247,7 +239,8 @@
final int displayId = activityPresentationInfo.displayId;
final ComponentName componentName = activityPresentationInfo.componentName;
final boolean whiteListed = mMaster.mGlobalContentCaptureOptions.isWhitelisted(mUserId,
- componentName);
+ componentName) || mMaster.mGlobalContentCaptureOptions.isWhitelisted(mUserId,
+ componentName.getPackageName());
final ComponentName serviceComponentName = getServiceComponentName();
final boolean enabled = isEnabledLocked();
if (mMaster.mRequestsHistory != null) {
@@ -462,40 +455,6 @@
@GuardedBy("mLock")
@Nullable
- ContentCaptureOptions getOptionsForPackageLocked(@NonNull String packageName) {
- if (!mWhitelistHelper.isWhitelisted(packageName)) {
- if (packageName.equals(getServicePackageName())) {
- if (mMaster.verbose) Slog.v(mTag, "getOptionsForPackage() lite for " + packageName);
- return new ContentCaptureOptions(mMaster.mDevCfgLoggingLevel);
- }
- if (mMaster.verbose) {
- Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted");
- }
- return null;
- }
-
- final ArraySet<ComponentName> whitelistedComponents = mWhitelistHelper
- .getWhitelistedComponents(packageName);
- if (Build.IS_USER && isTemporaryServiceSetLocked()) {
- final String servicePackageName = getServicePackageName();
- if (!packageName.equals(servicePackageName)) {
- Slog.w(mTag, "Ignoring package " + packageName
- + " while using temporary service " + servicePackageName);
- return null;
- }
- }
- final ContentCaptureOptions options = new ContentCaptureOptions(mMaster.mDevCfgLoggingLevel,
- mMaster.mDevCfgMaxBufferSize, mMaster.mDevCfgIdleFlushingFrequencyMs,
- mMaster.mDevCfgTextChangeFlushingFrequencyMs, mMaster.mDevCfgLogHistorySize,
- whitelistedComponents);
- if (mMaster.verbose) {
- Slog.v(mTag, "getOptionsForPackage(" + packageName + "): " + options);
- }
- return options;
- }
-
- @GuardedBy("mLock")
- @Nullable
ArraySet<ContentCaptureCondition> getContentCaptureConditionsLocked(
@NonNull String packageName) {
return mConditionsByPkg.get(packageName);