Implemented ContentCaptureConditions APIs.
Test: atest ContentCaptureConditionTest \
CustomViewActivityTest#testContentCaptureConditions \
CustomViewActivityTest#testContentCaptureConditions_otherPackage
Test: atest CtsContentCaptureServiceTestCases # sanity check (minus usual flakiness)
Fixes: 129266058
Change-Id: I199c3ae99fa6b407da64562a71d8d7581ebf80e6
diff --git a/core/java/android/content/ContentCaptureOptions.java b/core/java/android/content/ContentCaptureOptions.java
index 1727d34..76c4fb8 100644
--- a/core/java/android/content/ContentCaptureOptions.java
+++ b/core/java/android/content/ContentCaptureOptions.java
@@ -136,13 +136,18 @@
@Override
public String toString() {
if (lite) {
- return "ContentCaptureOptions [(lite) loggingLevel=" + loggingLevel + "]";
+ return "ContentCaptureOptions [loggingLevel=" + loggingLevel + " (lite)]";
}
- return "ContentCaptureOptions [loggingLevel=" + loggingLevel + ", maxBufferSize="
- + maxBufferSize + ", idleFlushingFrequencyMs=" + idleFlushingFrequencyMs
- + ", textChangeFlushingFrequencyMs=" + textChangeFlushingFrequencyMs
- + ", logHistorySize=" + logHistorySize + ", whitelistedComponents="
- + whitelistedComponents + "]";
+ final StringBuilder string = new StringBuilder("ContentCaptureOptions [");
+ string.append("loggingLevel=").append(loggingLevel)
+ .append(", maxBufferSize=").append(maxBufferSize)
+ .append(", idleFlushingFrequencyMs=").append(idleFlushingFrequencyMs)
+ .append(", textChangeFlushingFrequencyMs=").append(textChangeFlushingFrequencyMs)
+ .append(", logHistorySize=").append(logHistorySize);
+ if (whitelistedComponents != null) {
+ string.append(", whitelisted=").append(whitelistedComponents);
+ }
+ return string.append(']').toString();
}
/** @hide */
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index f83090c..dc57a15 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -17,6 +17,7 @@
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -54,7 +55,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -241,11 +241,17 @@
*/
public final void setContentCaptureConditions(@NonNull String packageName,
@Nullable Set<ContentCaptureCondition> conditions) {
- // TODO(b/129267994): implement
- }
+ final IContentCaptureServiceCallback callback = mCallback;
+ if (callback == null) {
+ Log.w(TAG, "setContentCaptureConditions(): no server callback");
+ return;
+ }
- private <T> ArrayList<T> toList(@Nullable Set<T> set) {
- return set == null ? null : new ArrayList<T>(set);
+ try {
+ callback.setContentCaptureConditions(packageName, toList(conditions));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
}
/**
diff --git a/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
index 8bc8def..0550ad3 100644
--- a/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
+++ b/core/java/android/service/contentcapture/IContentCaptureServiceCallback.aidl
@@ -17,6 +17,7 @@
package android.service.contentcapture;
import android.content.ComponentName;
+import android.view.contentcapture.ContentCaptureCondition;
import java.util.List;
@@ -27,5 +28,6 @@
*/
oneway interface IContentCaptureServiceCallback {
void setContentCaptureWhitelist(in List<String> packages, in List<ComponentName> activities);
+ void setContentCaptureConditions(String packageName, in List<ContentCaptureCondition> conditions);
void disableSelf();
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureCondition.aidl b/core/java/android/view/contentcapture/ContentCaptureCondition.aidl
new file mode 100644
index 0000000..99f8894
--- /dev/null
+++ b/core/java/android/view/contentcapture/ContentCaptureCondition.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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.view.contentcapture;
+
+parcelable ContentCaptureCondition;
diff --git a/core/java/android/view/contentcapture/ContentCaptureCondition.java b/core/java/android/view/contentcapture/ContentCaptureCondition.java
index ed87257..cf171d7 100644
--- a/core/java/android/view/contentcapture/ContentCaptureCondition.java
+++ b/core/java/android/view/contentcapture/ContentCaptureCondition.java
@@ -20,6 +20,7 @@
import android.content.LocusId;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.DebugUtils;
import com.android.internal.util.Preconditions;
@@ -58,7 +59,6 @@
public ContentCaptureCondition(@NonNull LocusId locusId, @Flags int flags) {
this.mLocusId = Preconditions.checkNotNull(locusId);
this.mFlags = flags;
- // TODO(b/129267994): check flags, add test case for null and invalid flags
}
/**
@@ -79,6 +79,42 @@
}
@Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mFlags;
+ result = prime * result + ((mLocusId == null) ? 0 : mLocusId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final ContentCaptureCondition other = (ContentCaptureCondition) obj;
+ if (mFlags != other.mFlags) return false;
+ if (mLocusId == null) {
+ if (other.mLocusId != null) return false;
+ } else {
+ if (!mLocusId.equals(other.mLocusId)) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder string = new StringBuilder(mLocusId.toString()); // LocusID is PII safe
+ if (mFlags != 0) {
+ string
+ .append(" (")
+ .append(DebugUtils.flagsToString(ContentCaptureCondition.class, "FLAG_", mFlags))
+ .append(')');
+ }
+ return string.toString();
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureHelper.java b/core/java/android/view/contentcapture/ContentCaptureHelper.java
index 6bc3829..c7ca220 100644
--- a/core/java/android/view/contentcapture/ContentCaptureHelper.java
+++ b/core/java/android/view/contentcapture/ContentCaptureHelper.java
@@ -23,9 +23,14 @@
import android.annotation.Nullable;
import android.os.Build;
import android.provider.DeviceConfig;
+import android.util.ArraySet;
import android.util.Log;
import android.view.contentcapture.ContentCaptureManager.LoggingLevel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
/**
* Helper class for this package and server's.
*
@@ -101,6 +106,22 @@
}
}
+ /**
+ * Converts a set to a list.
+ */
+ @Nullable
+ public static <T> ArrayList<T> toList(@Nullable Set<T> set) {
+ return set == null ? null : new ArrayList<T>(set);
+ }
+
+ /**
+ * Converts a list to a set.
+ */
+ @Nullable
+ public static <T> ArraySet<T> toSet(@Nullable List<T> list) {
+ return list == null ? null : new ArraySet<T>(list);
+ }
+
private ContentCaptureHelper() {
throw new UnsupportedOperationException("contains only static methods");
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 4f3325c..35f8023 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -17,6 +17,7 @@
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureHelper.toSet;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -46,6 +47,7 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Set;
/**
@@ -518,7 +520,20 @@
*/
@Nullable
public Set<ContentCaptureCondition> getContentCaptureConditions() {
- return null; // TODO(b/129267994): implement
+ // NOTE: we could cache the conditions on ContentCaptureOptions, but then it would be stick
+ // to the lifetime of the app. OTOH, by dynamically calling the server every time, we allow
+ // the service to fine tune how long-lived apps (like browsers) are whitelisted.
+ if (!isContentCaptureEnabled() && !mOptions.lite) return null;
+
+ final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
+ try {
+ mService.getContentCaptureConditions(mContext.getPackageName(), resultReceiver);
+ final ArrayList<ContentCaptureCondition> result = resultReceiver
+ .getParcelableListResult();
+ return toSet(result);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
index 2775029..7335073 100644
--- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
@@ -72,4 +72,9 @@
* Returns a ComponentName with the name of custom service activity, if defined.
*/
void getServiceSettingsActivity(in IResultReceiver result);
+
+ /**
+ * Returns a list with the ContentCaptureConditions for the package (or null if not defined).
+ */
+ void getContentCaptureConditions(String packageName, in IResultReceiver result);
}
diff --git a/core/java/com/android/internal/util/SyncResultReceiver.java b/core/java/com/android/internal/util/SyncResultReceiver.java
index 60af511..6fad84b 100644
--- a/core/java/com/android/internal/util/SyncResultReceiver.java
+++ b/core/java/com/android/internal/util/SyncResultReceiver.java
@@ -23,6 +23,7 @@
import com.android.internal.os.IResultReceiver;
+import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -97,6 +98,15 @@
}
/**
+ * Gets the result from an operation that returns a {@code Parcelable} list.
+ */
+ @Nullable
+ public <P extends Parcelable> ArrayList<P> getParcelableListResult() throws TimeoutException {
+ waitResult();
+ return mBundle == null ? null : mBundle.getParcelableArrayList(EXTRA);
+ }
+
+ /**
* Gets the optional result from an operation that returns an extra {@code int} (besides the
* result code).
*
@@ -150,6 +160,17 @@
}
/**
+ * Creates a bundle for a {@code Parcelable} list so it can be retrieved by
+ * {@link #getParcelableResult()}.
+ */
+ @NonNull
+ public static Bundle bundleFor(@Nullable ArrayList<? extends Parcelable> value) {
+ final Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(EXTRA, value);
+ return bundle;
+ }
+
+ /**
* Creates a bundle for an {@code int} value so it can be retrieved by
* {@link #getParcelableResult()} - typically used to return an extra {@code int} (as the 1st
* is returned as the result code).
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index c88d3ae..2894662 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
+import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
@@ -56,6 +57,7 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.ContentCaptureHelper;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.IContentCaptureManager;
@@ -568,6 +570,8 @@
@Override
public void removeUserData(@NonNull UserDataRemovalRequest request) {
Preconditions.checkNotNull(request);
+ // TODO(b/122959591): check caller uid owns the package name
+
final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
@@ -621,6 +625,25 @@
}
@Override
+ public void getContentCaptureConditions(@NonNull String packageName,
+ @NonNull IResultReceiver result) {
+ // TODO(b/122959591): check caller uid owns the package name
+
+ final int userId = UserHandle.getCallingUserId();
+ final ArrayList<ContentCaptureCondition> conditions;
+ synchronized (mLock) {
+ final ContentCapturePerUserService service = getServiceForUserLocked(userId);
+ conditions = service == null ? null
+ : toList(service.getContentCaptureConditionsLocked(packageName));
+ }
+ try {
+ result.send(RESULT_CODE_OK, bundleFor(conditions));
+ } catch (RemoteException e) {
+ Slog.w(mTag, "Unable to send getServiceComponentName(): " + e);
+ }
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(getContext(), mTag, pw)) return;
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 665d3df..5649526 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -35,11 +35,13 @@
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;
@@ -50,8 +52,11 @@
import android.service.contentcapture.ContentCaptureServiceInfo;
import android.service.contentcapture.IContentCaptureServiceCallback;
import android.service.contentcapture.SnapshotData;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.UserDataRemovalRequest;
import com.android.internal.annotations.GuardedBy;
@@ -98,6 +103,13 @@
private final WhitelistHelper mWhitelistHelper = new WhitelistHelper();
/**
+ * List of conditions keyed by package.
+ */
+ @GuardedBy("mLock")
+ private final ArrayMap<String, ArraySet<ContentCaptureCondition>> mConditionsByPkg =
+ new ArrayMap<>();
+
+ /**
* When {@code true}, remote service died but service state is kept so it's restored after
* the system re-binds to it.
*/
@@ -449,6 +461,47 @@
}
@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);
+ }
+
+ @GuardedBy("mLock")
void onActivityEventLocked(@NonNull ComponentName componentName, @ActivityEventType int type) {
if (mRemoteService == null) {
if (mMaster.debug) Slog.d(mTag, "onActivityEvent(): no remote service");
@@ -536,6 +589,23 @@
}
@Override
+ public void setContentCaptureConditions(String packageName,
+ List<ContentCaptureCondition> conditions) {
+ if (mMaster.verbose) {
+ Slog.v(TAG, "setContentCaptureConditions(" + packageName + "): "
+ + (conditions == null ? "null" : conditions.size() + " conditions"));
+ }
+ synchronized (mLock) {
+ if (conditions == null) {
+ mConditionsByPkg.remove(packageName);
+ } else {
+ mConditionsByPkg.put(packageName, new ArraySet<>(conditions));
+ }
+ }
+ // TODO(b/119613670): log metrics
+ }
+
+ @Override
public void disableSelf() {
if (mMaster.verbose) Slog.v(TAG, "disableSelf()");