diff options
10 files changed, 213 insertions, 80 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 0d22f3a8bb03..c8bd27503a1c 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -127,6 +127,7 @@ import android.view.autofill.AutofillPopupWindow; 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 @@ public class Activity extends ContextThemeWrapper 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 @@ public class Activity extends ContextThemeWrapper 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 @@ public class Activity extends ContextThemeWrapper 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 98b658d3ad25..c48c878fc366 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -1143,7 +1143,7 @@ final class SystemServiceRegistry { 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 76c4fb8caa0b..cb2142c356b2 100644 --- a/core/java/android/content/ContentCaptureOptions.java +++ b/core/java/android/content/ContentCaptureOptions.java @@ -24,6 +24,9 @@ import android.os.Parcelable; 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 class ContentCaptureOptions implements Parcelable { */ 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 @@ public final class ContentCaptureOptions implements Parcelable { 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 @@ public final class ContentCaptureOptions implements Parcelable { this.whitelistedComponents = whitelistedComponents; } - /** - * @hide - */ - @TestApi public static ContentCaptureOptions forWhitelistingItself() { final ActivityThread at = ActivityThread.currentActivityThread(); if (at == null) { @@ -120,19 +136,27 @@ public final class ContentCaptureOptions implements Parcelable { 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 de048290c0ca..00238bfe0ee3 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -70,6 +70,7 @@ import android.view.View; 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 @@ public abstract class Context { /** * @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 26454c055932..7a6e2adfcaa4 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -343,6 +343,15 @@ public final class ContentCaptureManager { 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 dfa59b7bd0ac..a0b2f94aea32 100644 --- a/core/java/com/android/internal/infra/GlobalWhitelistState.java +++ b/core/java/com/android/internal/infra/GlobalWhitelistState.java @@ -35,11 +35,13 @@ import java.util.List; * * <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 d7753db6b0f7..9d653bad4d00 100644 --- a/core/java/com/android/internal/infra/WhitelistHelper.java +++ b/core/java/com/android/internal/infra/WhitelistHelper.java @@ -98,9 +98,9 @@ public final class WhitelistHelper { @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 @@ public final class WhitelistHelper { 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 000000000000..c6f4fa27dc83 --- /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 7f411d83b34b..a2d3d4c25b1d 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 @@ public final class ContentCaptureManagerService extends @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"); - } - return null; } + } // synchronized - 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; - } + // 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 ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel, - mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs, - mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize, - whitelistedComponents); + } + + if (!packageWhitelisted && whitelistedComponents == null) { + // No can do! if (verbose) { - Slog.v(mTag, "getOptionsForPackage(" + packageName + "): " + options); + Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted"); } - return options; + 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 5a42e7893ab4..a7921b5f3892 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.ActivityManagerInternal; 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.ContentCaptureCondition; import android.view.contentcapture.DataRemovalRequest; 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 @@ final class ContentCapturePerUserService 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 @@ final class ContentCapturePerUserService * 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 class ContentCapturePerUserService 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 @@ final class ContentCapturePerUserService @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); |