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
Merged-In: I2c76a01bd98c4154c4c59099f1368232d2dba80d
Change-Id: I2c76a01bd98c4154c4c59099f1368232d2dba80d
This commit is contained in:
Felipe Leme
2019-04-17 13:57:59 -07:00
parent d4cced9ab6
commit cbf7f26baa
10 changed files with 216 additions and 83 deletions

View File

@@ -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

View File

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

View File

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

View File

@@ -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;
@@ -5411,6 +5412,14 @@ public abstract class Context {
public void setAutofillClient(@SuppressWarnings("unused") AutofillClient client) {
}
/**
* @hide
*/
@Nullable
public ContentCaptureClient getContentCaptureClient() {
return null;
}
/**
* @hide
*/

View File

@@ -342,6 +342,15 @@ public final class ContentCaptureManager {
@GuardedBy("mLock")
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) {

View File

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

View File

@@ -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("]");
}

View File

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

View File

@@ -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");
}
}
} // 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

View File

@@ -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;
@@ -96,12 +93,6 @@ final class ContentCapturePerUserService
private final ContentCaptureServiceRemoteCallback mRemoteServiceCallback =
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.
*/
@@ -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) {
@@ -460,40 +453,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(