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