package android.content {
+ public final class ContentCaptureOptions implements android.os.Parcelable {
+ ctor public ContentCaptureOptions(int, int, int, int, int, @Nullable android.util.ArraySet<android.content.ComponentName>);
+ method public int describeContents();
+ method public static android.content.ContentCaptureOptions forWhitelistingItself();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.content.ContentCaptureOptions> CREATOR;
+ field public final int idleFlushingFrequencyMs;
+ field public final int logHistorySize;
+ field public final int loggingLevel;
+ field public final int maxBufferSize;
+ field public final int textChangeFlushingFrequencyMs;
+ field @Nullable public final android.util.ArraySet<android.content.ComponentName> whitelistedComponents;
+ }
+
public class ContentProviderClient implements java.lang.AutoCloseable {
method @RequiresPermission(android.Manifest.permission.REMOVE_TASKS) public void setDetectNotResponding(long);
}
method public android.os.UserHandle getUser();
method public int getUserId();
method public void setAutofillCompatibilityEnabled(boolean);
+ method public void setContentCaptureOptions(@Nullable android.content.ContentCaptureOptions);
}
}
super.attachBaseContext(newBase);
if (newBase != null) {
newBase.setAutofillClient(this);
- newBase.setContentCaptureSupported(true);
+ newBase.setContentCaptureOptions(getContentCaptureOptions());
}
}
return this;
}
- /** @hide */
- @Override
- public boolean isContentCaptureSupported() {
- return true;
- }
-
/**
* Register an {@link Application.ActivityLifecycleCallbacks} instance that receives
* lifecycle callbacks for only this Activity.
mWindow.setColorMode(info.colorMode);
setAutofillCompatibilityEnabled(application.isAutofillCompatibilityEnabled());
+ setContentCaptureOptions(application.getContentCaptureOptions());
}
private void enableAutofillCompatibilityIfNeeded() {
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
boolean autofillCompatibilityEnabled;
+ /**
+ * Content capture options for the application - when null, it means ContentCapture is not
+ * enabled for the package.
+ */
+ @Nullable
+ ContentCaptureOptions contentCaptureOptions;
+
+ @Override
public String toString() {
return "AppBindData{appInfo=" + appInfo + "}";
}
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
- String buildSerial, boolean autofillCompatibilityEnabled) {
+ String buildSerial, boolean autofillCompatibilityEnabled,
+ ContentCaptureOptions contentCaptureOptions) {
if (services != null) {
if (false) {
data.initProfilerInfo = profilerInfo;
data.buildSerial = buildSerial;
data.autofillCompatibilityEnabled = autofillCompatibilityEnabled;
+ data.contentCaptureOptions = contentCaptureOptions;
sendMessage(H.BIND_APPLICATION, data);
}
// Propagate autofill compat state
app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);
+ // Propagate Content Capture options
+ app.setContentCaptureOptions(data.contentCaptureOptions);
+
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
import android.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
private AutofillClient mAutofillClient = null;
private boolean mIsAutofillCompatEnabled;
- private boolean mIsContentCaptureSupported = false;
+ private ContentCaptureOptions mContentCaptureOptions = null;
private final Object mSync = new Object();
/** @hide */
@Override
- public boolean isContentCaptureSupported() {
- return mIsContentCaptureSupported;
+ public ContentCaptureOptions getContentCaptureOptions() {
+ return mContentCaptureOptions;
}
/** @hide */
@Override
- public void setContentCaptureSupported(boolean supported) {
- mIsContentCaptureSupported = supported;
+ public void setContentCaptureOptions(ContentCaptureOptions options) {
+ mContentCaptureOptions = options;
}
@UnsupportedAppUsage
import android.app.ResultInfo;
import android.app.servertransaction.ClientTransaction;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
int debugMode, boolean enableBinderTracking, boolean trackAllocation,
boolean restrictedBackupMode, boolean persistent, in Configuration config,
in CompatibilityInfo compatInfo, in Map services,
- in Bundle coreSettings, in String buildSerial, boolean isAutofillCompatEnabled);
+ in Bundle coreSettings, in String buildSerial, boolean isAutofillCompatEnabled,
+ in ContentCaptureOptions contentCaptureOptions);
void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
void scheduleExit();
void scheduleServiceArgs(IBinder token, in ParceledListSlice args);
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceManager;
import android.content.ClipboardManager;
+import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.IRestrictionsManager;
import android.content.RestrictionsManager;
throws ServiceNotFoundException {
// Get the services without throwing as this is an optional feature
Context outerContext = ctx.getOuterContext();
- if (outerContext.isContentCaptureSupported()) {
+ ContentCaptureOptions options = outerContext.getContentCaptureOptions();
+ // Options is null when the service didn't whitelist the activity or package
+ if (options != null) {
IBinder b = ServiceManager
.getService(Context.CONTENT_CAPTURE_MANAGER_SERVICE);
IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(b);
+ // Service is null when not provided by OEM or disabled by kill-switch.
if (service != null) {
- // When feature is disabled, we return a null manager to apps so the
- // performance impact is practically zero
- return new ContentCaptureManager(outerContext, service);
+ return new ContentCaptureManager(outerContext, service, options);
}
}
+ // When feature is disabled or app / package not whitelisted, we return a null
+ // manager to apps so the performance impact is practically zero
return null;
}});
--- /dev/null
+/*
+** Copyright 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;
+
+parcelable ContentCaptureOptions;
--- /dev/null
+/*
+ * Copyright (C) 2018 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.contentcapture.ContentCaptureManager;
+
+import java.io.PrintWriter;
+
+/**
+ * Content capture options for a given package.
+ *
+ * <p>This object is created by the Content Capture System Service and passed back to the app when
+ * the application is created.
+ *
+ * @hide
+ */
+@TestApi
+public final class ContentCaptureOptions implements Parcelable {
+
+ private static final String TAG = ContentCaptureOptions.class.getSimpleName();
+
+ /**
+ * Logging level for {@code logcat} statements.
+ */
+ public final int loggingLevel;
+
+ /**
+ * Maximum number of events that are buffered before sent to the app.
+ */
+ public final int maxBufferSize;
+
+ /**
+ * Frequency the buffer is flushed if idle.
+ */
+ public final int idleFlushingFrequencyMs;
+
+ /**
+ * Frequency the buffer is flushed if last event is a text change.
+ */
+ public final int textChangeFlushingFrequencyMs;
+
+ /**
+ * Size of events that are logging on {@code dump}.
+ */
+ public final int logHistorySize;
+
+ /**
+ * List of activities explicitly whitelisted for content capture (or {@code null} if whitelisted
+ * for all acitivites in the package).
+ */
+ @Nullable
+ public final ArraySet<ComponentName> whitelistedComponents;
+
+ public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
+ int textChangeFlushingFrequencyMs, int logHistorySize,
+ @Nullable ArraySet<ComponentName> whitelistedComponents) {
+ this.loggingLevel = loggingLevel;
+ this.maxBufferSize = maxBufferSize;
+ this.idleFlushingFrequencyMs = idleFlushingFrequencyMs;
+ this.textChangeFlushingFrequencyMs = textChangeFlushingFrequencyMs;
+ this.logHistorySize = logHistorySize;
+ this.whitelistedComponents = whitelistedComponents;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public static ContentCaptureOptions forWhitelistingItself() {
+ final ActivityThread at = ActivityThread.currentActivityThread();
+ if (at == null) {
+ throw new IllegalStateException("No ActivityThread");
+ }
+
+ final String packageName = at.getApplication().getPackageName();
+
+ if (!"android.contentcaptureservice.cts".equals(packageName)) {
+ Log.e(TAG, "forWhitelistingItself(): called by " + packageName);
+ 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);
+ // Always log, as it's used by test only
+ Log.i(TAG, "forWhitelistingItself(" + packageName + "): " + options);
+
+ return options;
+ }
+
+ @Override
+ public String toString() {
+ return "ContentCaptureOptions [loggingLevel=" + loggingLevel + ", maxBufferSize="
+ + maxBufferSize + ", idleFlushingFrequencyMs=" + idleFlushingFrequencyMs
+ + ", textChangeFlushingFrequencyMs=" + textChangeFlushingFrequencyMs
+ + ", logHistorySize=" + logHistorySize + ", whitelistedComponents="
+ + whitelistedComponents + "]";
+ }
+
+ /** @hide */
+ public void dumpShort(@NonNull PrintWriter pw) {
+ pw.print("logLvl="); pw.print(loggingLevel);
+ pw.print(", bufferSize="); pw.print(maxBufferSize);
+ pw.print(", idle="); pw.print(idleFlushingFrequencyMs);
+ pw.print(", textIdle="); pw.print(textChangeFlushingFrequencyMs);
+ pw.print(", logSize="); pw.print(logHistorySize);
+ if (whitelistedComponents != null) {
+ pw.print(", whitelisted="); pw.print(whitelistedComponents);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(loggingLevel);
+ parcel.writeInt(maxBufferSize);
+ parcel.writeInt(idleFlushingFrequencyMs);
+ parcel.writeInt(textChangeFlushingFrequencyMs);
+ parcel.writeInt(logHistorySize);
+ parcel.writeArraySet(whitelistedComponents);
+ }
+
+ public static final Parcelable.Creator<ContentCaptureOptions> CREATOR =
+ new Parcelable.Creator<ContentCaptureOptions>() {
+
+ @Override
+ public ContentCaptureOptions createFromParcel(Parcel parcel) {
+ final int loggingLevel = parcel.readInt();
+ final int maxBufferSize = parcel.readInt();
+ final int idleFlushingFrequencyMs = parcel.readInt();
+ final int textChangeFlushingFrequencyMs = parcel.readInt();
+ final int logHistorySize = parcel.readInt();
+ @SuppressWarnings("unchecked")
+ final ArraySet<ComponentName> whitelistedComponents =
+ (ArraySet<ComponentName>) parcel.readArraySet(null);
+ return new ContentCaptureOptions(loggingLevel, maxBufferSize,
+ idleFlushingFrequencyMs, textChangeFlushingFrequencyMs, logHistorySize,
+ whitelistedComponents);
+ }
+
+ @Override
+ public ContentCaptureOptions[] newArray(int size) {
+ return new ContentCaptureOptions[size];
+ }
+
+ };
+}
}
/**
- * Checks whether this context supports content capture.
+ * Gets the Content Capture options for this context, or {@code null} if it's not whitelisted.
*
* @hide
*/
- // NOTE: for now we just need to check if it's supported so we can optimize calls that can be
- // skipped when it isn't. Eventually, we might need a full
- // ContentCaptureManager.ContentCaptureClient interface (as it's done with AutofillClient).
- //
- public boolean isContentCaptureSupported() {
- return false;
+ @Nullable
+ public ContentCaptureOptions getContentCaptureOptions() {
+ return null;
}
/**
* @hide
*/
- public void setContentCaptureSupported(@SuppressWarnings("unused") boolean supported) {
+ @TestApi
+ public void setContentCaptureOptions(
+ @SuppressWarnings("unused") @Nullable ContentCaptureOptions options) {
}
/**
*/
@TestApi
@Override
- public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
+ public void setAutofillCompatibilityEnabled(boolean autofillCompatEnabled) {
if (mBase != null) {
mBase.setAutofillCompatibilityEnabled(autofillCompatEnabled);
}
* @hide
*/
@Override
- public boolean isContentCaptureSupported() {
- return mBase.isContentCaptureSupported();
+ public ContentCaptureOptions getContentCaptureOptions() {
+ return mBase == null ? null : mBase.getContentCaptureOptions();
}
/**
* @hide
*/
+ @TestApi
@Override
- public void setContentCaptureSupported(boolean supported) {
- mBase.setContentCaptureSupported(supported);
+ public void setContentCaptureOptions(ContentCaptureOptions options) {
+ if (mBase != null) {
+ mBase.setContentCaptureOptions(options);
+ }
}
}
*/
package android.service.contentcapture;
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.CallSuper;
private static final String TAG = ContentCaptureService.class.getSimpleName();
- // TODO(b/121044306): STOPSHIP use dynamic value, or change to false
- static final boolean DEBUG = true;
- static final boolean VERBOSE = false;
-
/**
* The {@link Intent} that must be declared as handled by the service.
*
private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
@Override
- public void onConnected(IBinder callback) {
+ public void onConnected(IBinder callback, boolean verbose, boolean debug) {
+ sVerbose = verbose;
+ sDebug = debug;
mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected,
ContentCaptureService.this, callback));
}
*/
public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
@NonNull ContentCaptureSessionId sessionId) {
- if (VERBOSE) {
+ if (sVerbose) {
Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
}
}
@Deprecated
public void onContentCaptureEventsRequest(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEventsRequest request) {
- if (VERBOSE) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
+ if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
}
/**
*/
public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEvent event) {
- if (VERBOSE) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
+ if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
onContentCaptureEventsRequest(sessionId, new ContentCaptureEventsRequest(event));
}
* @param request the user data requested to be removed
*/
public void onUserDataRemovalRequest(@NonNull UserDataRemovalRequest request) {
- if (VERBOSE) Log.v(TAG, "onUserDataRemovalRequest()");
+ if (sVerbose) Log.v(TAG, "onUserDataRemovalRequest()");
}
/**
* @param sessionId the id of the session to destroy
* */
public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
- if (VERBOSE) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
+ if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
}
/**
* Disables the Content Capture service for the given user.
*/
public final void disableContentCaptureServices() {
- if (DEBUG) Log.d(TAG, "disableContentCaptureServices()");
+ if (sDebug) Log.d(TAG, "disableContentCaptureServices()");
final IContentCaptureServiceCallback callback = mCallback;
if (callback == null) {
@Override
@CallSuper
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
final int size = mSessionUids.size();
pw.print("Number sessions: "); pw.println(size);
if (size > 0) {
}
final Integer rightUid = mSessionUids.get(sessionId);
if (rightUid == null) {
- if (VERBOSE) {
+ if (sVerbose) {
Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
+ ": " + mSessionUids);
}
* @hide
*/
oneway interface IContentCaptureService {
- void onConnected(IBinder callback);
+ void onConnected(IBinder callback, boolean verbose, boolean debug);
void onDisconnected();
void onSessionStarted(in ContentCaptureContext context, String sessionId, int uid,
in IResultReceiver clientReceiver);
AttachInfo ai = mAttachInfo;
// First check if context has client, so it saves a service lookup when it doesn't
- if (!mContext.isContentCaptureSupported()) return;
+ if (mContext.getContentCaptureOptions() == null) return;
// Then check if it's enabled in the context...
final ContentCaptureManager ccm = ai != null ? ai.getContentCaptureManager(mContext)
}
try {
// First check if context supports it, so it saves a service lookup when it doesn't
- if (!mContext.isContentCaptureSupported()) return;
+ if (mContext.getContentCaptureOptions() == null) return;
// Then check if it's enabled in the contex itself.
final ContentCaptureManager ccm = mContext
}
/**
+ * Gets the default logging level for the device.
+ */
+ @LoggingLevel
+ public static int getDefaultLoggingLevel() {
+ return Build.IS_DEBUGGABLE ? LOGGING_LEVEL_DEBUG : LOGGING_LEVEL_OFF;
+ }
+
+ /**
* Sets the value of the static logging level constants based on device config.
*/
public static void setLoggingLevel() {
- final int defaultLevel = Build.IS_DEBUGGABLE ? LOGGING_LEVEL_DEBUG : LOGGING_LEVEL_OFF;
+ final int defaultLevel = getDefaultLoggingLevel();
final int level = getIntDeviceConfigProperty(DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
defaultLevel);
+ setLoggingLevel(level);
+ }
+
+ /**
+ * Sets the value of the static logging level constants based the given level.
+ */
+ public static void setLoggingLevel(@LoggingLevel int level) {
Log.i(TAG, "Setting logging level to " + getLoggingLevelAsString(level));
sVerbose = sDebug = false;
switch (level) {
import android.annotation.TestApi;
import android.annotation.UiThread;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
@Retention(RetentionPolicy.SOURCE)
public @interface LoggingLevel {}
+
+ /** @hide */
+ public static final int DEFAULT_MAX_BUFFER_SIZE = 100;
+ /** @hide */
+ public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000;
+ /** @hide */
+ public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000;
+ /** @hide */
+ public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
+
private final Object mLock = new Object();
@NonNull
@NonNull
private final IContentCaptureManager mService;
+ @NonNull
+ final ContentCaptureOptions mOptions;
+
// Flags used for starting session.
@GuardedBy("mLock")
private int mFlags;
/** @hide */
public ContentCaptureManager(@NonNull Context context,
- @NonNull IContentCaptureManager service) {
+ @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
mContext = Preconditions.checkNotNull(context, "context cannot be null");
mService = Preconditions.checkNotNull(service, "service cannot be null");
+ mOptions = Preconditions.checkNotNull(options, "options cannot be null");
- // TODO(b/123096662): right now we're reading the device config values here, but ideally
- // it should be read on ContentCaptureManagerService and passed back when the activity
- // started.
- ContentCaptureHelper.setLoggingLevel();
+ ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel);
if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
synchronized (mLock) {
pw.print(prefix2); pw.print("isContentCaptureEnabled(): ");
pw.println(isContentCaptureEnabled());
- pw.print(prefix); pw.print("Debug: "); pw.print(sDebug);
+ pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug);
pw.print(" Verbose: "); pw.println(sVerbose);
- pw.print(prefix); pw.print("Context: "); pw.println(mContext);
- pw.print(prefix); pw.print("User: "); pw.println(mContext.getUserId());
- pw.print(prefix); pw.print("Service: "); pw.println(mService);
- pw.print(prefix); pw.print("Flags: "); pw.println(mFlags);
+ pw.print(prefix2); pw.print("Context: "); pw.println(mContext);
+ pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId());
+ pw.print(prefix2); pw.print("Service: "); pw.println(mService);
+ pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags);
+ pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println();
if (mMainSession != null) {
final String prefix3 = prefix2 + " ";
pw.print(prefix2); pw.println("Main session:");
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
-import static android.view.contentcapture.ContentCaptureHelper.getIntDeviceConfigProperty;
import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE;
-import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE;
import android.annotation.NonNull;
import android.annotation.Nullable;
*/
private static final int MSG_FLUSH = 1;
- private static final int DEFAULT_MAX_BUFFER_SIZE = 100;
- private static final int DEFAULT_FLUSHING_FREQUENCY_MS = 5_000;
- private static final int DEFAULT_LOG_HISTORY_SIZE = 10;
-
/**
* Name of the {@link IResultReceiver} extra used to pass the binder interface to the service.
* @hide
@Nullable
private ArrayList<ContentCaptureEvent> mEvents;
- /**
- * Maximum number of events that are buffered before sent to the app.
- */
- private final int mMaxBufferSize;
-
- /**
- * Frequency the buffer is flushed if idle.
- */
- private final int mIdleFlushingFrequencyMs;
-
// Used just for debugging purposes (on dump)
private long mNextFlush;
mHandler = handler;
mSystemServerInterface = systemServerInterface;
- // TODO(b/123096662): right now we're reading the device config values here, but ideally
- // it should be read on ContentCaptureManagerService and passed back when the activity
- // started.
- mMaxBufferSize = getIntDeviceConfigProperty(DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
- DEFAULT_MAX_BUFFER_SIZE);
- mIdleFlushingFrequencyMs = getIntDeviceConfigProperty(
- DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY, DEFAULT_FLUSHING_FREQUENCY_MS);
- final int logHistorySize = getIntDeviceConfigProperty(
- DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, DEFAULT_LOG_HISTORY_SIZE);
-
+ final int logHistorySize = mManager.mOptions.logHistorySize;
mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
}
if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
return;
}
+ final int maxBufferSize = mManager.mOptions.maxBufferSize;
if (mEvents == null) {
if (sVerbose) {
- Log.v(TAG, "handleSendEvent(): creating buffer for " + mMaxBufferSize + " events");
+ Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
}
- mEvents = new ArrayList<>(mMaxBufferSize);
+ mEvents = new ArrayList<>(maxBufferSize);
}
// Some type of events can be merged together
final int numberEvents = mEvents.size();
- final boolean bufferEvent = numberEvents < mMaxBufferSize;
+ final boolean bufferEvent = numberEvents < maxBufferSize;
if (bufferEvent && !forceFlush) {
scheduleFlush(FLUSH_REASON_IDLE_TIMEOUT, /* checkExisting= */ true);
return;
}
- if (mState != STATE_ACTIVE && numberEvents >= mMaxBufferSize) {
+ if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
// Callback from startSession hasn't been called yet - typically happens on system
// apps that are started before the system service
// TODO(b/122959591): try to ignore session while system is not ready / boot
// "Renew" the flush message by removing the previous one
mHandler.removeMessages(MSG_FLUSH);
}
- mNextFlush = System.currentTimeMillis() + mIdleFlushingFrequencyMs;
+ final int idleFlushingFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
+ mNextFlush = System.currentTimeMillis() + idleFlushingFrequencyMs;
if (sVerbose) {
Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
- + mIdleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
+ + idleFlushingFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
}
// Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
- mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, mIdleFlushingFrequencyMs);
+ mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, idleFlushingFrequencyMs);
}
@UiThread
if (mFlushHistory != null) {
// Logs reason, size, max size, idle timeout
final String logRecord = "r=" + reasonString + " s=" + numberEvents
- + " m=" + mMaxBufferSize + " i=" + mIdleFlushingFrequencyMs;
+ + " m=" + mManager.mOptions.maxBufferSize
+ + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
mFlushHistory.log(logRecord);
}
try {
pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
- pw.print(prefix); pw.print("mSystemServerInterface: ");
if (mDirectServiceInterface != null) {
pw.print(prefix); pw.print("mDirectServiceInterface: ");
pw.println(mDirectServiceInterface);
if (mEvents != null && !mEvents.isEmpty()) {
final int numberEvents = mEvents.size();
pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
- pw.print('/'); pw.println(mMaxBufferSize);
+ pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
if (sVerbose && numberEvents > 0) {
final String prefix3 = prefix + " ";
for (int i = 0; i < numberEvents; i++) {
pw.println();
}
}
- pw.print(prefix); pw.print("flush frequency: "); pw.println(mIdleFlushingFrequencyMs);
+ pw.print(prefix); pw.print("flush frequency: ");
+ pw.println(mManager.mOptions.idleFlushingFrequencyMs);
pw.print(prefix); pw.print("next flush: ");
TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
package com.android.internal.policy;
+import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
}
@Override
- public boolean isContentCaptureSupported() {
- return true;
+ public ContentCaptureOptions getContentCaptureOptions() {
+ Context activityContext = mActivityContext.get();
+ if (activityContext != null) {
+ return activityContext.getContentCaptureOptions();
+ }
+ return null;
}
}
import android.app.IUiAutomationConnection;
import android.app.ProfilerInfo;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
boolean b2, boolean b3, Configuration configuration,
CompatibilityInfo compatibilityInfo, Map map, Bundle bundle1, String s1,
- boolean autofillCompatEnabled) throws RemoteException {
+ boolean autofillCompatEnabled, ContentCaptureOptions o) throws RemoteException {
}
@Override
@Before
public void before() {
- mManager = new ContentCaptureManager(mMockContext, null);
+ mManager = new ContentCaptureManager(mMockContext, /* service= */ null,
+ /* options= */ null);
}
@Test
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityPresentationInfo;
@Nullable
private boolean mDisabledByDeviceConfig;
+ // Device-config settings that are cached and passed back to apps
+ public int mDevCfgLoggingLevel;
+ public int mDevCfgMaxBufferSize;
+ public int mDevCfgIdleFlushingFrequencyMs;
+ public int mDevCfgTextChangeFlushingFrequencyMs;
+ public int mDevCfgLogHistorySize;
+
public ContentCaptureManagerService(@NonNull Context context) {
super(context, new FrameworkResourcesServiceNameResolver(context,
com.android.internal.R.string.config_defaultContentCaptureService),
DeviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ActivityThread.currentApplication().getMainExecutor(),
(namespace, key, value) -> onDeviceConfigChange(key, value));
- setLoggingLevelFromDeviceConfig();
- setDisabledFromDeviceConfig();
+ setDeviceConfigProperties();
- final int loggingSize = ContentCaptureHelper.getIntDeviceConfigProperty(
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
- if (loggingSize > 0) {
- if (debug) Slog.d(mTag, "log history size: " + loggingSize);
- mRequestsHistory = new LocalLog(loggingSize);
+ if (mDevCfgLogHistorySize > 0) {
+ if (debug) Slog.d(mTag, "log history size: " + mDevCfgLogHistorySize);
+ mRequestsHistory = new LocalLog(mDevCfgLogHistorySize);
} else {
- if (debug) Slog.d(mTag, "disabled log history because size is " + loggingSize);
+ if (debug) {
+ Slog.d(mTag, "disabled log history because size is " + mDevCfgLogHistorySize);
+ }
mRequestsHistory = null;
}
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE:
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY:
- // TODO(b/123096662): implement it
- Slog.d(mTag, "changes on " + key + " not supported yet");
+ setFineTuneParamsFromDeviceConfig();
return;
default:
Slog.i(mTag, "Ignoring change on " + key);
}
}
+ private void setFineTuneParamsFromDeviceConfig() {
+ mDevCfgMaxBufferSize = ContentCaptureHelper.getIntDeviceConfigProperty(
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
+ ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
+ mDevCfgIdleFlushingFrequencyMs = ContentCaptureHelper.getIntDeviceConfigProperty(
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
+ ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
+ mDevCfgTextChangeFlushingFrequencyMs = ContentCaptureHelper.getIntDeviceConfigProperty(
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
+ ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
+ mDevCfgLogHistorySize = ContentCaptureHelper.getIntDeviceConfigProperty(
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
+ if (verbose) {
+ Slog.v(mTag, "setFineTuneParamsFromDeviceConfig(): bufferSize=" + mDevCfgMaxBufferSize
+ + ", idleFlush=" + mDevCfgIdleFlushingFrequencyMs
+ + ", textFluxh=" + mDevCfgTextChangeFlushingFrequencyMs
+ + ", logHistory=" + mDevCfgLogHistorySize);
+ }
+ }
+
private void setLoggingLevelFromDeviceConfig() {
- ContentCaptureHelper.setLoggingLevel();
+ mDevCfgLoggingLevel = ContentCaptureHelper.getIntDeviceConfigProperty(
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
+ ContentCaptureHelper.getDefaultLoggingLevel());
+ ContentCaptureHelper.setLoggingLevel(mDevCfgLoggingLevel);
verbose = ContentCaptureHelper.sVerbose;
debug = ContentCaptureHelper.sDebug;
+ if (verbose) {
+ Slog.v(mTag, "setLoggingLevelFromDeviceConfig(): level=" + mDevCfgLoggingLevel
+ + ", debug=" + debug + ", verbose=" + verbose);
+ }
}
- private void setDisabledFromDeviceConfig() {
- final String value = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ private void setDeviceConfigProperties() {
+ setLoggingLevelFromDeviceConfig();
+ setFineTuneParamsFromDeviceConfig();
+ final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
- setDisabledByDeviceConfig(value);
+ setDisabledByDeviceConfig(enabled);
}
- private void setDisabledByDeviceConfig(@Nullable String value) {
- if (verbose) Slog.v(mTag, "setDisabledByDeviceConfig(): value=" + value);
+ private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
+ if (verbose) {
+ Slog.v(mTag, "setDisabledByDeviceConfig(): explicitlyEnabled=" + explicitlyEnabled);
+ }
final UserManager um = getContext().getSystemService(UserManager.class);
final List<UserInfo> users = um.getUsers();
final boolean newDisabledValue;
- if (value != null && value.equalsIgnoreCase("false")) {
+ if (explicitlyEnabled != null && explicitlyEnabled.equalsIgnoreCase("false")) {
newDisabledValue = true;
} else {
newDisabledValue = false;
protected void dumpLocked(String prefix, PrintWriter pw) {
super.dumpLocked(prefix, pw);
+ final String prefix2 = prefix + " ";
+
pw.print(prefix); pw.print("Disabled users: "); pw.println(mDisabledUsers);
- pw.print(prefix); pw.print("Disabled by DeviceConfig: ");
- pw.println(mDisabledByDeviceConfig);
+ pw.print(prefix); pw.println("DeviceConfig Settings: ");
+ pw.print(prefix2); pw.print("disabled: "); pw.println(mDisabledByDeviceConfig);
+ pw.print(prefix2); pw.print("loggingLevel: "); pw.println(mDevCfgLoggingLevel);
+ pw.print(prefix2); pw.print("maxBufferSize: "); pw.println(mDevCfgMaxBufferSize);
+ pw.print(prefix2); pw.print("idleFlushingFrequencyMs: ");
+ pw.println(mDevCfgIdleFlushingFrequencyMs);
+ pw.print(prefix2); pw.print("textChangeFlushingFrequencyMs: ");
+ pw.println(mDevCfgTextChangeFlushingFrequencyMs);
+ pw.print(prefix2); pw.print("logHistorySize: "); pw.println(mDevCfgLogHistorySize);
}
final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {
pw.println();
mRequestsHistory.reverseDump(fd, pw, args);
pw.println();
+ } else {
+ pw.println();
}
}
}
return false;
}
+
+ @Override
+ public ContentCaptureOptions getOptionsForPackage(int userId, String packageName) {
+ synchronized (mLock) {
+ final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.getOptionsForPackageLocked(packageName);
+ }
+ }
+ return null;
+ }
}
}
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;
}
}
+ @GuardedBy("mLock")
+ ContentCaptureOptions getOptionsForPackageLocked(@NonNull String packageName) {
+ if (!mWhitelistedPackages.contains(packageName)) {
+ if (mMaster.verbose) {
+ Slog.v(mTag, "getOptionsForPackage(" + packageName + "): not whitelisted");
+ }
+ return null;
+ }
+
+ // TODO(b/122595322): need to check whitelisted activities as well.
+ final ArraySet<ComponentName> whitelistedComponents = null;
+ 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;
+ }
+
@Override
protected void dumpLocked(String prefix, PrintWriter pw) {
super.dumpLocked(prefix, pw);
*/
package com.android.server.contentcapture;
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
}
try {
if (state) {
- mService.onConnected(mServerCallback);
+ mService.onConnected(mServerCallback, sVerbose, sDebug);
} else {
mService.onDisconnected();
}
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import com.android.server.am.ActivityManagerServiceDumpProcessesProto.UidObserverRegistrationProto;
import com.android.server.am.MemoryStatUtil.MemoryStat;
import com.android.server.appop.AppOpsService;
+import com.android.server.contentcapture.ContentCaptureManagerInternal;
import com.android.server.firewall.IntentFirewall;
import com.android.server.job.JobSchedulerInternal;
import com.android.server.pm.Installer;
app.info.packageName, app.info.versionCode, app.userId);
}
}
+ ContentCaptureOptions contentCaptureOptions = null;
+ if (UserHandle.getAppId(app.info.uid) >= Process.FIRST_APPLICATION_UID) {
+ final ContentCaptureManagerInternal ccm =
+ LocalServices.getService(ContentCaptureManagerInternal.class);
+ if (ccm != null) {
+ contentCaptureOptions = ccm.getOptionsForPackage(app.userId,
+ app.info.packageName);
+ }
+ }
checkTime(startTime, "attachApplicationLocked: immediately before bindApplication");
bindApplicationTimeMillis = SystemClock.elapsedRealtime();
new Configuration(app.getWindowProcessController().getConfiguration()),
app.compat, getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
- buildSerial, isAutofillCompatEnabled);
+ buildSerial, isAutofillCompatEnabled, contentCaptureOptions);
} else {
thread.bindApplication(processName, appInfo, providers, null, profilerInfo,
null, null, null, testMode,
new Configuration(app.getWindowProcessController().getConfiguration()),
app.compat, getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
- buildSerial, isAutofillCompatEnabled);
+ buildSerial, isAutofillCompatEnabled, contentCaptureOptions);
}
if (profilerInfo != null) {
profilerInfo.closeFd();
package com.android.server.contentcapture;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.content.ContentCaptureOptions;
import android.os.Bundle;
import android.os.IBinder;
*/
public abstract boolean sendActivityAssistData(@UserIdInt int userId,
@NonNull IBinder activityToken, @NonNull Bundle data);
+
+ /**
+ * Gets the content capture options for the given user and package, or {@code null} if the
+ * package is not whitelisted by the service.
+ */
+ @Nullable
+ public abstract ContentCaptureOptions getOptionsForPackage(@UserIdInt int userId,
+ @NonNull String packageName);
}