@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 */
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;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.List;
import java.util.Set;
*/
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();
+ }
}
/**
package android.service.contentcapture;
import android.content.ComponentName;
+import android.view.contentcapture.ContentCaptureCondition;
import java.util.List;
*/
oneway interface IContentCaptureServiceCallback {
void setContentCaptureWhitelist(in List<String> packages, in List<ComponentName> activities);
+ void setContentCaptureConditions(String packageName, in List<ContentCaptureCondition> conditions);
void disableSelf();
}
--- /dev/null
+/**
+ * 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;
import android.content.LocusId;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.DebugUtils;
import com.android.internal.util.Preconditions;
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
}
/**
}
@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;
}
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.
*
}
}
+ /**
+ * 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");
}
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;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Set;
/**
*/
@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();
+ }
}
/**
* 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);
}
import com.android.internal.os.IResultReceiver;
+import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
}
/**
+ * 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).
*
}
/**
+ * 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).
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;
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;
@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);
}
@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;
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;
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;
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.
*/
}
@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");
}
@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()");