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;
+    }
+}