summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt45
-rw-r--r--api/system-current.txt58
-rw-r--r--core/java/android/app/Activity.java50
-rw-r--r--core/java/android/content/ComponentName.java11
-rw-r--r--core/java/android/service/contentcapture/ContentCaptureService.java42
-rw-r--r--core/java/android/service/contentcapture/IContentCaptureService.aidl4
-rw-r--r--core/java/android/service/contentcapture/InteractionContext.java157
-rw-r--r--core/java/android/view/View.java100
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureContext.aidl (renamed from core/java/android/service/contentcapture/InteractionContext.aidl)4
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureContext.java333
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureEvent.java65
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureManager.java502
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureSession.java570
-rw-r--r--core/java/android/view/contentcapture/ContentCaptureSessionId.java (renamed from core/java/android/service/contentcapture/InteractionSessionId.java)24
-rw-r--r--core/java/android/view/contentcapture/IContentCaptureManager.aidl4
-rw-r--r--core/java/android/view/contentcapture/UserDataRemovalRequest.java167
-rw-r--r--core/java/android/widget/TextView.java12
-rw-r--r--services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java6
-rw-r--r--services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java41
-rw-r--r--services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java (renamed from services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java)27
-rw-r--r--services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java14
21 files changed, 1468 insertions, 768 deletions
diff --git a/api/current.txt b/api/current.txt
index 63bbad29cc0a..66524e06a15d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -49548,6 +49548,7 @@ package android.view {
method public android.graphics.Rect getClipBounds();
method public boolean getClipBounds(android.graphics.Rect);
method public final boolean getClipToOutline();
+ method public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession();
method public java.lang.CharSequence getContentDescription();
method public final android.content.Context getContext();
method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
@@ -49884,6 +49885,7 @@ package android.view {
method public void setClickable(boolean);
method public void setClipBounds(android.graphics.Rect);
method public void setClipToOutline(boolean);
+ method public void setContentCaptureSession(android.view.contentcapture.ContentCaptureSession);
method public void setContentDescription(java.lang.CharSequence);
method public void setContextClickable(boolean);
method public void setDefaultFocusHighlightEnabled(boolean);
@@ -52144,17 +52146,56 @@ package android.view.autofill {
package android.view.contentcapture {
+ public final class ContentCaptureContext implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.contentcapture.ContentCaptureContext> CREATOR;
+ }
+
+ public static final class ContentCaptureContext.Builder {
+ ctor public ContentCaptureContext.Builder();
+ method public android.view.contentcapture.ContentCaptureContext build();
+ method public android.view.contentcapture.ContentCaptureContext.Builder setExtras(android.os.Bundle);
+ method public android.view.contentcapture.ContentCaptureContext.Builder setUri(android.net.Uri);
+ }
+
public final class ContentCaptureManager {
+ method public android.view.contentcapture.ContentCaptureSession createContentCaptureSession(android.view.contentcapture.ContentCaptureContext);
method public android.content.ComponentName getServiceComponentName();
method public boolean isContentCaptureEnabled();
- method public android.view.ViewStructure newVirtualViewStructure(android.view.autofill.AutofillId, int);
+ method public void removeUserData(android.view.contentcapture.UserDataRemovalRequest);
+ method public void setContentCaptureEnabled(boolean);
+ }
+
+ public final class ContentCaptureSession implements java.lang.AutoCloseable {
+ method public void close();
+ method public void destroy();
+ method public android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId();
method public void notifyViewAppeared(android.view.ViewStructure);
method public void notifyViewDisappeared(android.view.autofill.AutofillId);
method public void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int);
- method public void setContentCaptureEnabled(boolean);
field public static final int FLAG_USER_INPUT = 1; // 0x1
}
+ public final class ContentCaptureSessionId implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.contentcapture.ContentCaptureSessionId> CREATOR;
+ }
+
+ public final class UserDataRemovalRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.contentcapture.UserDataRemovalRequest> CREATOR;
+ }
+
+ public static final class UserDataRemovalRequest.Builder {
+ ctor public UserDataRemovalRequest.Builder();
+ method public android.view.contentcapture.UserDataRemovalRequest.Builder addUri(android.net.Uri, boolean);
+ method public android.view.contentcapture.UserDataRemovalRequest build();
+ method public android.view.contentcapture.UserDataRemovalRequest.Builder forEverything();
+ }
+
}
package android.view.inputmethod {
diff --git a/api/system-current.txt b/api/system-current.txt
index cc93bbafd046..26e1eab857cb 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4984,34 +4984,16 @@ package android.service.contentcapture {
ctor public ContentCaptureService();
method public final java.util.Set<android.content.ComponentName> getContentCaptureDisabledActivities();
method public final java.util.Set<java.lang.String> getContentCaptureDisabledPackages();
- method public void onActivitySnapshot(android.service.contentcapture.InteractionSessionId, android.service.contentcapture.SnapshotData);
- method public abstract void onContentCaptureEventsRequest(android.service.contentcapture.InteractionSessionId, android.service.contentcapture.ContentCaptureEventsRequest);
- method public void onCreateInteractionSession(android.service.contentcapture.InteractionContext, android.service.contentcapture.InteractionSessionId);
- method public void onDestroyInteractionSession(android.service.contentcapture.InteractionSessionId);
+ method public void onActivitySnapshot(android.view.contentcapture.ContentCaptureSessionId, android.service.contentcapture.SnapshotData);
+ method public abstract void onContentCaptureEventsRequest(android.view.contentcapture.ContentCaptureSessionId, android.service.contentcapture.ContentCaptureEventsRequest);
+ method public void onCreateContentCaptureSession(android.view.contentcapture.ContentCaptureContext, android.view.contentcapture.ContentCaptureSessionId);
+ method public void onDestroyContentCaptureSession(android.view.contentcapture.ContentCaptureSessionId);
method public final void setActivityContentCaptureEnabled(android.content.ComponentName, boolean);
method public final void setContentCaptureWhitelist(java.util.List<java.lang.String>, java.util.List<android.content.ComponentName>);
method public final void setPackageContentCaptureEnabled(java.lang.String, boolean);
field public static final java.lang.String SERVICE_INTERFACE = "android.service.contentcapture.ContentCaptureService";
}
- public final class InteractionContext implements android.os.Parcelable {
- method public int describeContents();
- method public android.content.ComponentName getActivityComponent();
- method public int getDisplayId();
- method public int getFlags();
- method public int getTaskId();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.service.contentcapture.InteractionContext> CREATOR;
- field public static final int FLAG_DISABLED_BY_APP = 1; // 0x1
- field public static final int FLAG_DISABLED_BY_FLAG_SECURE = 2; // 0x2
- }
-
- public final class InteractionSessionId implements android.os.Parcelable {
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.service.contentcapture.InteractionSessionId> CREATOR;
- }
-
public final class SnapshotData implements android.os.Parcelable {
method public int describeContents();
method public android.app.assist.AssistContent getAssistContent();
@@ -7281,6 +7263,17 @@ package android.view.accessibility {
package android.view.contentcapture {
+ public final class ContentCaptureContext implements android.os.Parcelable {
+ method public android.content.ComponentName getActivityComponent();
+ method public int getDisplayId();
+ method public android.os.Bundle getExtras();
+ method public int getFlags();
+ method public int getTaskId();
+ method public android.net.Uri getUri();
+ field public static final int FLAG_DISABLED_BY_APP = 1; // 0x1
+ field public static final int FLAG_DISABLED_BY_FLAG_SECURE = 2; // 0x2
+ }
+
public final class ContentCaptureEvent implements android.os.Parcelable {
method public int describeContents();
method public long getEventTime();
@@ -7291,13 +7284,20 @@ package android.view.contentcapture {
method public android.view.contentcapture.ViewNode getViewNode();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.view.contentcapture.ContentCaptureEvent> CREATOR;
- field public static final deprecated int TYPE_ACTIVITY_PAUSED = 3; // 0x3
- field public static final deprecated int TYPE_ACTIVITY_RESUMED = 2; // 0x2
- field public static final deprecated int TYPE_ACTIVITY_STARTED = 1; // 0x1
- field public static final deprecated int TYPE_ACTIVITY_STOPPED = 4; // 0x4
- field public static final int TYPE_VIEW_APPEARED = 5; // 0x5
- field public static final int TYPE_VIEW_DISAPPEARED = 6; // 0x6
- field public static final int TYPE_VIEW_TEXT_CHANGED = 7; // 0x7
+ field public static final int TYPE_VIEW_APPEARED = 1; // 0x1
+ field public static final int TYPE_VIEW_DISAPPEARED = 2; // 0x2
+ field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3
+ }
+
+ public final class UserDataRemovalRequest implements android.os.Parcelable {
+ method public java.lang.String getPackageName();
+ method public java.util.List<android.view.contentcapture.UserDataRemovalRequest.UriRequest> getUriRequests();
+ method public boolean isForEverything();
+ }
+
+ public final class UserDataRemovalRequest.UriRequest {
+ method public android.net.Uri getUri();
+ method public boolean isRecursive();
}
public final class ViewNode extends android.app.assist.AssistStructure.ViewNode {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b584d5d4c3b4..48a767bee341 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -121,7 +121,6 @@ import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillManager.AutofillClient;
import android.view.autofill.AutofillPopupWindow;
import android.view.autofill.IAutofillWindowPresenter;
-import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureManager;
import android.widget.AdapterView;
import android.widget.Toast;
@@ -1027,28 +1026,39 @@ public class Activity extends ContextThemeWrapper
return mContentCaptureManager;
}
- private void notifyContentCaptureManagerIfNeeded(@ContentCaptureEvent.EventType int event) {
+ /** @hide */ private static final int CONTENT_CAPTURE_START = 1;
+ /** @hide */ private static final int CONTENT_CAPTURE_FLUSH = 2;
+ /** @hide */ private static final int CONTENT_CAPTURE_STOP = 3;
+
+ /** @hide */
+ @IntDef(prefix = { "CONTENT_CAPTURE_" }, value = {
+ CONTENT_CAPTURE_START,
+ CONTENT_CAPTURE_FLUSH,
+ CONTENT_CAPTURE_STOP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ContentCaptureNotificationType{}
+
+
+ private void notifyContentCaptureManagerIfNeeded(@ContentCaptureNotificationType int type) {
final ContentCaptureManager cm = getContentCaptureManager();
if (cm == null || !cm.isContentCaptureEnabled()) {
return;
}
- switch (event) {
- case ContentCaptureEvent.TYPE_ACTIVITY_CREATED:
+ switch (type) {
+ case CONTENT_CAPTURE_START:
//TODO(b/111276913): decide whether the InteractionSessionId should be
- // saved / restored in the activity bundle.
- cm.onActivityCreated(mToken, getComponentName());
+ // saved / restored in the activity bundle - probably not
+ cm.onActivityStarted(mToken, getComponentName());
break;
- case ContentCaptureEvent.TYPE_ACTIVITY_DESTROYED:
- cm.onActivityDestroyed();
+ case CONTENT_CAPTURE_FLUSH:
+ cm.flush();
break;
- case ContentCaptureEvent.TYPE_ACTIVITY_STARTED:
- case ContentCaptureEvent.TYPE_ACTIVITY_RESUMED:
- case ContentCaptureEvent.TYPE_ACTIVITY_PAUSED:
- case ContentCaptureEvent.TYPE_ACTIVITY_STOPPED:
- cm.onActivityLifecycleEvent(event);
+ case CONTENT_CAPTURE_STOP:
+ cm.onActivityStopped();
break;
default:
- Log.w(TAG, "notifyContentCaptureManagerIfNeeded(): invalid type " + event);
+ Log.wtf(TAG, "Invalid @ContentCaptureNotificationType: " + type);
}
}
@@ -1417,7 +1427,6 @@ public class Activity extends ContextThemeWrapper
mRestoredFromBundle = savedInstanceState != null;
mCalled = true;
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_CREATED);
}
/**
@@ -1651,7 +1660,7 @@ public class Activity extends ContextThemeWrapper
if (mAutoFillResetNeeded) {
getAutofillManager().onVisibleForAutofill();
}
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_STARTED);
+ notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_START);
}
/**
@@ -1734,8 +1743,8 @@ public class Activity extends ContextThemeWrapper
}
}
}
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_RESUMED);
mCalled = true;
+ notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_FLUSH);
}
/**
@@ -2128,8 +2137,8 @@ public class Activity extends ContextThemeWrapper
mAutoFillIgnoreFirstResumePause = false;
}
}
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_PAUSED);
mCalled = true;
+ notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_FLUSH);
}
/**
@@ -2317,7 +2326,7 @@ public class Activity extends ContextThemeWrapper
getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_CANCEL,
mIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN));
}
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_STOPPED);
+ notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_STOP);
}
}
@@ -2388,9 +2397,6 @@ public class Activity extends ContextThemeWrapper
}
dispatchActivityDestroyed();
-
- notifyContentCaptureManagerIfNeeded(ContentCaptureEvent.TYPE_ACTIVITY_DESTROYED);
-
}
/**
diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java
index 54e6342747db..e6ffe8b4fe86 100644
--- a/core/java/android/content/ComponentName.java
+++ b/core/java/android/content/ComponentName.java
@@ -192,6 +192,17 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co
}
/**
+ * Helper to get {@link #flattenToShortString()} in a {@link ComponentName} reference that can
+ * be {@code null}.
+ *
+ * @hide
+ */
+ @Nullable
+ public static String flattenToShortString(@Nullable ComponentName componentName) {
+ return componentName == null ? null : componentName.flattenToShortString();
+ }
+
+ /**
* Return a String that unambiguously describes both the package and
* class names contained in the ComponentName. You can later recover
* the ComponentName from this string through
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index 3dfeeded5946..58848fce43d6 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -29,7 +29,9 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ContentCaptureSessionId;
import java.util.List;
import java.util.Set;
@@ -45,7 +47,7 @@ public abstract class ContentCaptureService extends Service {
private static final String TAG = ContentCaptureService.class.getSimpleName();
- // TODO(b/111330312): STOPSHIP use dynamic value, or change to false
+ // TODO(b/121044306): STOPSHIP use dynamic value, or change to false
static final boolean DEBUG = true;
static final boolean VERBOSE = false;
@@ -64,15 +66,15 @@ public abstract class ContentCaptureService extends Service {
private final IContentCaptureService mInterface = new IContentCaptureService.Stub() {
@Override
- public void onSessionLifecycle(InteractionContext context, String sessionId)
+ public void onSessionLifecycle(ContentCaptureContext context, String sessionId)
throws RemoteException {
if (context != null) {
mHandler.sendMessage(
- obtainMessage(ContentCaptureService::handleOnCreateInteractionSession,
+ obtainMessage(ContentCaptureService::handleOnCreateSession,
ContentCaptureService.this, context, sessionId));
} else {
mHandler.sendMessage(
- obtainMessage(ContentCaptureService::handleOnDestroyInteractionSession,
+ obtainMessage(ContentCaptureService::handleOnDestroySession,
ContentCaptureService.this, sessionId));
}
}
@@ -175,15 +177,15 @@ public abstract class ContentCaptureService extends Service {
}
/**
- * Creates a new interaction session.
+ * Creates a new content capture session.
*
- * @param context interaction context
+ * @param context content capture context
* @param sessionId the session's Id
*/
- public void onCreateInteractionSession(@NonNull InteractionContext context,
- @NonNull InteractionSessionId sessionId) {
+ public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
+ @NonNull ContentCaptureSessionId sessionId) {
if (VERBOSE) {
- Log.v(TAG, "onCreateInteractionSession(id=" + sessionId + ", ctx=" + context + ")");
+ Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
}
}
@@ -194,7 +196,7 @@ public abstract class ContentCaptureService extends Service {
* @param sessionId the session's Id
* @param request the events
*/
- public abstract void onContentCaptureEventsRequest(@NonNull InteractionSessionId sessionId,
+ public abstract void onContentCaptureEventsRequest(@NonNull ContentCaptureSessionId sessionId,
@NonNull ContentCaptureEventsRequest request);
/**
@@ -203,39 +205,39 @@ public abstract class ContentCaptureService extends Service {
* @param sessionId the session's Id
* @param snapshotData the data
*/
- public void onActivitySnapshot(@NonNull InteractionSessionId sessionId,
+ public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId,
@NonNull SnapshotData snapshotData) {}
/**
- * Destroys the interaction session.
+ * Destroys the content capture session.
*
* @param sessionId the id of the session to destroy
*/
- public void onDestroyInteractionSession(@NonNull InteractionSessionId sessionId) {
+ public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
if (VERBOSE) {
- Log.v(TAG, "onDestroyInteractionSession(id=" + sessionId + ")");
+ Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
}
}
//TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session,
// so we don't need to create a temporary InteractionSessionId for each event.
- private void handleOnCreateInteractionSession(@NonNull InteractionContext context,
+ private void handleOnCreateSession(@NonNull ContentCaptureContext context,
@NonNull String sessionId) {
- onCreateInteractionSession(context, new InteractionSessionId(sessionId));
+ onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
}
private void handleOnContentCaptureEventsRequest(@NonNull String sessionId,
@NonNull ContentCaptureEventsRequest request) {
- onContentCaptureEventsRequest(new InteractionSessionId(sessionId), request);
+ onContentCaptureEventsRequest(new ContentCaptureSessionId(sessionId), request);
}
private void handleOnActivitySnapshot(@NonNull String sessionId,
@NonNull SnapshotData snapshotData) {
- onActivitySnapshot(new InteractionSessionId(sessionId), snapshotData);
+ onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
}
- private void handleOnDestroyInteractionSession(@NonNull String sessionId) {
- onDestroyInteractionSession(new InteractionSessionId(sessionId));
+ private void handleOnDestroySession(@NonNull String sessionId) {
+ onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
}
}
diff --git a/core/java/android/service/contentcapture/IContentCaptureService.aidl b/core/java/android/service/contentcapture/IContentCaptureService.aidl
index 29e9abbc6263..8167be9e6838 100644
--- a/core/java/android/service/contentcapture/IContentCaptureService.aidl
+++ b/core/java/android/service/contentcapture/IContentCaptureService.aidl
@@ -17,8 +17,8 @@
package android.service.contentcapture;
import android.service.contentcapture.ContentCaptureEventsRequest;
-import android.service.contentcapture.InteractionContext;
import android.service.contentcapture.SnapshotData;
+import android.view.contentcapture.ContentCaptureContext;
import java.util.List;
@@ -30,7 +30,7 @@ import java.util.List;
oneway interface IContentCaptureService {
// Called when session is created (context not null) or destroyed (context null)
- void onSessionLifecycle(in InteractionContext context, String sessionId);
+ void onSessionLifecycle(in ContentCaptureContext context, String sessionId);
void onContentCaptureEventsRequest(String sessionId, in ContentCaptureEventsRequest request);
diff --git a/core/java/android/service/contentcapture/InteractionContext.java b/core/java/android/service/contentcapture/InteractionContext.java
deleted file mode 100644
index f1281ff01033..000000000000
--- a/core/java/android/service/contentcapture/InteractionContext.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.service.contentcapture;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.SystemApi;
-import android.app.TaskInfo;
-import android.content.ComponentName;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-// TODO(b/111276913): add javadocs / implement Parcelable / implement equals/hashcode/toString
-/** @hide */
-@SystemApi
-public final class InteractionContext implements Parcelable {
-
- /**
- * Flag used to indicate that the app explicitly disabled content capture for the activity
- * (using
- * {@link android.view.contentcapture.ContentCaptureManager#setContentCaptureEnabled(boolean)}),
- * in which case the service will just receive activity-level events.
- */
- public static final int FLAG_DISABLED_BY_APP = 0x1;
-
- /**
- * Flag used to indicate that the activity's window is tagged with
- * {@link android.view.Display#FLAG_SECURE}, in which case the service will just receive
- * activity-level events.
- */
- public static final int FLAG_DISABLED_BY_FLAG_SECURE = 0x2;
-
- /** @hide */
- @IntDef(flag = true, prefix = { "FLAG_" }, value = {
- FLAG_DISABLED_BY_APP,
- FLAG_DISABLED_BY_FLAG_SECURE
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface ContextCreationFlags{}
-
- // TODO(b/111276913): create new object for taskId + componentName / reuse on other places
- private final @NonNull ComponentName mComponentName;
- private final int mTaskId;
- private final int mDisplayId;
- private final int mFlags;
-
-
- /** @hide */
- public InteractionContext(@NonNull ComponentName componentName, int taskId, int displayId,
- int flags) {
- mComponentName = Preconditions.checkNotNull(componentName);
- mTaskId = taskId;
- mDisplayId = displayId;
- mFlags = flags;
- }
-
- /**
- * Gets the id of the {@link TaskInfo task} associated with this context.
- */
- public int getTaskId() {
- return mTaskId;
- }
-
- /**
- * Gets the activity associated with this context.
- */
- public @NonNull ComponentName getActivityComponent() {
- return mComponentName;
- }
-
- /**
- * Gets the ID of the display associated with this context, as defined by
- * {G android.hardware.display.DisplayManager#getDisplay(int) DisplayManager.getDisplay()}.
- */
- public int getDisplayId() {
- return mDisplayId;
- }
-
- /**
- * Gets the flags associated with this context.
- *
- * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE} and
- * {@link #FLAG_DISABLED_BY_APP}.
- */
- public @ContextCreationFlags int getFlags() {
- return mFlags;
- }
-
- /**
- * @hide
- */
- // TODO(b/111276913): dump to proto as well
- public void dump(PrintWriter pw) {
- pw.print("comp="); pw.print(mComponentName.flattenToShortString());
- pw.print(", taskId="); pw.print(mTaskId);
- pw.print(", displayId="); pw.print(mDisplayId);
- if (mFlags > 0) {
- pw.print(", flags="); pw.print(mFlags);
- }
- }
-
- @Override
- public String toString() {
- return "Context[act=" + mComponentName.flattenToShortString() + ", taskId=" + mTaskId
- + ", displayId=" + mDisplayId + ", flags=" + mFlags + "]";
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeParcelable(mComponentName, flags);
- parcel.writeInt(mTaskId);
- parcel.writeInt(mDisplayId);
- parcel.writeInt(mFlags);
- }
-
- public static final Parcelable.Creator<InteractionContext> CREATOR =
- new Parcelable.Creator<InteractionContext>() {
-
- @Override
- public InteractionContext createFromParcel(Parcel parcel) {
- final ComponentName componentName = parcel.readParcelable(null);
- final int taskId = parcel.readInt();
- final int displayId = parcel.readInt();
- final int flags = parcel.readInt();
- return new InteractionContext(componentName, taskId, displayId, flags);
- }
-
- @Override
- public InteractionContext[] newArray(int size) {
- return new InteractionContext[size];
- }
- };
-}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 4297efb71ad0..468d92290c13 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -109,7 +109,9 @@ import android.view.animation.Transformation;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
@@ -121,6 +123,7 @@ import android.widget.FrameLayout;
import android.widget.ScrollBarDrawable;
import com.android.internal.R;
+import com.android.internal.util.Preconditions;
import com.android.internal.view.TooltipPopup;
import com.android.internal.view.menu.MenuBuilder;
import com.android.internal.widget.ScrollBarUtils;
@@ -5018,6 +5021,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private Handler mVisibilityChangeForAutofillHandler;
/**
+ * Used when app developers explicitly set the {@link ContentCaptureSession} associated with the
+ * view (through {@link #setContentCaptureSession(ContentCaptureSession)}.
+ */
+ @Nullable
+ private WeakReference<ContentCaptureSession> mContentCaptureSession;
+
+ /**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
@@ -8161,7 +8171,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* is visible.
*
* <p>The populated structure is then passed to the service through
- * {@link ContentCaptureManager#notifyViewAppeared(ViewStructure)}.
+ * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}.
*
* <p><b>Note: </b>the following methods of the {@code structure} will be ignored:
* <ul>
@@ -8977,13 +8987,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (!mContext.isContentCaptureSupported()) return;
// Then check if it's enabled in the context...
- final ContentCaptureManager cm = mContext.getSystemService(ContentCaptureManager.class);
- if (cm == null || !cm.isContentCaptureEnabled()) return;
+ final ContentCaptureManager ccm = mContext.getSystemService(ContentCaptureManager.class);
+ if (ccm == null || !ccm.isContentCaptureEnabled()) return;
// ... and finally at the view level
// NOTE: isImportantForContentCapture() is more expensive than cm.isContentCaptureEnabled()
if (!isImportantForContentCapture()) return;
+ final ContentCaptureSession session = getContentCaptureSession(ccm);
+ if (session == null) return;
+
if (appeared) {
if (!isLaidOut() || !isVisibleToUser()
|| (mPrivateFlags4 & PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED) != 0) {
@@ -8995,10 +9008,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
return;
}
- // All good: notify the manager...
- final ViewStructure structure = cm.newViewStructure(this);
+ // All good: notify it...
+ final ViewStructure structure = session.newViewStructure(this);
onProvideContentCaptureStructure(structure, /* flags= */ 0);
- cm.notifyViewAppeared(structure);
+ session.notifyViewAppeared(structure);
// ...and set the flags
mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED;
mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED;
@@ -9014,14 +9027,85 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
return;
}
- // All good: notify the manager...
- cm.notifyViewDisappeared(getAutofillId());
+ // All good: notify it...
+ session.notifyViewDisappeared(getAutofillId());
// ...and set the flags
mPrivateFlags4 |= PFLAG4_NOTIFIED_CONTENT_CAPTURE_DISAPPEARED;
mPrivateFlags4 &= ~PFLAG4_NOTIFIED_CONTENT_CAPTURE_APPEARED;
}
}
+ /**
+ * Sets the (optional) {@link ContentCaptureSession} associated with this view.
+ *
+ * <p>This method should be called when you need to associate a {@link ContentCaptureContext} to
+ * the Content Capture events associated with this view or its view hierarchy (if it's a
+ * {@link ViewGroup}).
+ *
+ * <p>For example, if your activity is associated with a web domain, you could create a session
+ * {@code onCreate()} and associate it with the root view of the activity:
+ *
+ * <pre>
+ * ContentCaptureManager mgr = getSystemService(ContentCaptureManager.class);
+ * if (mgr != null && mgr.isContentCaptureEnabled()) {
+ * View rootView = findViewById(R.my_root_view);
+ * ContentCaptureSession session = mgr.createContentCaptureSession(new
+ * ContentCaptureContext.Builder().setUri(myUrl).build());
+ * rootView.setContentCaptureSession(session);
+ * }
+ * </pre>
+ *
+ * @param contentCaptureSession a session created by
+ * {@link ContentCaptureManager#createContentCaptureSession(
+ * android.view.contentcapture.ContentCaptureContext)}.
+ */
+ public void setContentCaptureSession(@NonNull ContentCaptureSession contentCaptureSession) {
+ mContentCaptureSession = new WeakReference<>(
+ Preconditions.checkNotNull(contentCaptureSession));
+ }
+
+ /**
+ * Gets the session used to notify Content Capture events.
+ *
+ * @return session explicitly set by {@link #setContentCaptureSession(ContentCaptureSession)},
+ * inherited by ancestore, default session or {@code null} if content capture is disabled for
+ * this view.
+ */
+ @Nullable
+ public final ContentCaptureSession getContentCaptureSession() {
+ // First try the session explicitly set by setContentCaptureSession()
+ if (mContentCaptureSession != null) return mContentCaptureSession.get();
+
+ // Then the session explicitly set in an ancestor
+ ContentCaptureSession session = null;
+ if (mParent instanceof View) {
+ session = ((View) mParent).getContentCaptureSession();
+ }
+
+ // Finally, if no session was explicitly set, use the context's default session.
+ if (session == null) {
+ final ContentCaptureManager ccm = mContext
+ .getSystemService(ContentCaptureManager.class);
+ return ccm == null ? null : ccm.getMainContentCaptureSession();
+ }
+ return session;
+ }
+
+ /**
+ * Optimized version of {@link #getContentCaptureSession()} that avoids a service lookup.
+ */
+ @Nullable
+ private ContentCaptureSession getContentCaptureSession(@NonNull ContentCaptureManager ccm) {
+ if (mContentCaptureSession != null) return mContentCaptureSession.get();
+
+ ContentCaptureSession session = null;
+ if (mParent instanceof View) {
+ session = ((View) mParent).getContentCaptureSession();
+ }
+
+ return session != null ? session : ccm.getMainContentCaptureSession();
+ }
+
@Nullable
private AutofillManager getAutofillManager() {
return mContext.getSystemService(AutofillManager.class);
diff --git a/core/java/android/service/contentcapture/InteractionContext.aidl b/core/java/android/view/contentcapture/ContentCaptureContext.aidl
index 982e095894fb..da492f502818 100644
--- a/core/java/android/service/contentcapture/InteractionContext.aidl
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.aidl
@@ -14,6 +14,6 @@
* limitations under the License.
*/
-package android.service.contentcapture;
+package android.view.contentcapture;
-parcelable InteractionContext;
+parcelable ContentCaptureContext;
diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java
new file mode 100644
index 000000000000..9c11743fdf19
--- /dev/null
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.java
@@ -0,0 +1,333 @@
+/*
+ * 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.view.contentcapture;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.TaskInfo;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Context associated with a {@link ContentCaptureSession}.
+ */
+public final class ContentCaptureContext implements Parcelable {
+
+ /*
+ * IMPLEMENTATION NOTICE:
+ *
+ * This object contains both the info that's explicitly added by apps (hence it's public), but
+ * it also contains info injected by the server (and are accessible through @SystemApi methods).
+ */
+
+ /**
+ * Flag used to indicate that the app explicitly disabled content capture for the activity
+ * (using
+ * {@link android.view.contentcapture.ContentCaptureManager#setContentCaptureEnabled(boolean)}),
+ * in which case the service will just receive activity-level events.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_DISABLED_BY_APP = 0x1;
+
+ /**
+ * Flag used to indicate that the activity's window is tagged with
+ * {@link android.view.Display#FLAG_SECURE}, in which case the service will just receive
+ * activity-level events.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_DISABLED_BY_FLAG_SECURE = 0x2;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_DISABLED_BY_APP,
+ FLAG_DISABLED_BY_FLAG_SECURE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ContextCreationFlags{}
+
+ /**
+ * Flag indicating if this object has the app-provided context (which is set on
+ * {@link ContentCaptureManager#createContentCaptureSession(ContentCaptureContext)}).
+ */
+ private final boolean mHasClientContext;
+
+ // Fields below are set by app on Builder
+ private final @Nullable Bundle mExtras;
+ private final @Nullable Uri mUri;
+
+ // Fields below are set by server when the session starts
+ // TODO(b/111276913): create new object for taskId + componentName / reuse on other places
+ private final @Nullable ComponentName mComponentName;
+ private final int mTaskId;
+ private final int mDisplayId;
+ private final int mFlags;
+
+ /** @hide */
+ public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
+ @NonNull ComponentName componentName, int taskId, int displayId, int flags) {
+ if (clientContext != null) {
+ mHasClientContext = true;
+ mExtras = clientContext.mExtras;
+ mUri = clientContext.mUri;
+ } else {
+ mHasClientContext = false;
+ mExtras = null;
+ mUri = null;
+ }
+ mComponentName = Preconditions.checkNotNull(componentName);
+ mTaskId = taskId;
+ mDisplayId = displayId;
+ mFlags = flags;
+ }
+
+ private ContentCaptureContext(@NonNull Builder builder) {
+ mHasClientContext = true;
+ mExtras = builder.mExtras;
+ mUri = builder.mUri;
+
+ mComponentName = null;
+ mTaskId = mFlags = mDisplayId = 0;
+ }
+
+ /**
+ * Gets the (optional) extras set by the app.
+ *
+ * <p>It can be used to provide vendor-specific data that can be modified and examined.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Gets the (optional) URI set by the app.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Gets the id of the {@link TaskInfo task} associated with this context.
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getTaskId() {
+ return mTaskId;
+ }
+
+ /**
+ * Gets the activity associated with this context.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull ComponentName getActivityComponent() {
+ return mComponentName;
+ }
+
+ /**
+ * Gets the ID of the display associated with this context, as defined by
+ * {G android.hardware.display.DisplayManager#getDisplay(int) DisplayManager.getDisplay()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * Gets the flags associated with this context.
+ *
+ * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE} and
+ * {@link #FLAG_DISABLED_BY_APP}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @ContextCreationFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Builder for {@link ContentCaptureContext} objects.
+ */
+ public static final class Builder {
+ private Bundle mExtras;
+ private Uri mUri;
+
+ /**
+ * Sets extra options associated with this context.
+ *
+ * <p>It can be used to provide vendor-specific data that can be modified and examined.
+ *
+ * @param extras extra options.
+ * @return this builder.
+ */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ // TODO(b/111276913): check build just once / throw exception / test / document
+ mExtras = Preconditions.checkNotNull(extras);
+ return this;
+ }
+
+ /**
+ * Sets the {@link Uri} associated with this context.
+ *
+ * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for an example.
+ *
+ * @param uri URI associated with this context.
+ * @return this builder.
+ */
+ @NonNull
+ public Builder setUri(@NonNull Uri uri) {
+ // TODO(b/111276913): check build just once / throw exception / test / document
+ mUri = Preconditions.checkNotNull(uri);
+ return this;
+ }
+
+ /**
+ * Builds the {@link ContentCaptureContext}.
+ */
+ public ContentCaptureContext build() {
+ // TODO(b/111276913): check build just once / throw exception / test / document
+ // TODO(b/111276913): make sure it at least one property (uri / extras) / test /
+ // throw exception / documment
+ return new ContentCaptureContext(this);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(b/111276913): dump to proto as well
+ public void dump(PrintWriter pw) {
+ pw.print("comp="); pw.print(ComponentName.flattenToShortString(mComponentName));
+ pw.print(", taskId="); pw.print(mTaskId);
+ pw.print(", displayId="); pw.print(mDisplayId);
+ if (mFlags > 0) {
+ pw.print(", flags="); pw.print(mFlags);
+ }
+ if (mExtras != null) {
+ // NOTE: cannot dump because it could contain PII
+ pw.print(", hasExtras");
+ }
+ if (mUri != null) {
+ // NOTE: cannot dump because it could contain PII
+ pw.print(", hasUri");
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("Context[act=")
+ .append(ComponentName.flattenToShortString(mComponentName))
+ .append(", taskId=").append(mTaskId)
+ .append(", displayId=").append(mDisplayId)
+ .append(", flags=").append(mFlags);
+ if (mExtras != null) {
+ // NOTE: cannot print because it could contain PII
+ builder.append(", hasExtras");
+ }
+ if (mUri != null) {
+ // NOTE: cannot print because it could contain PII
+ builder.append(", hasUri");
+ }
+ return builder.append(']').toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mHasClientContext ? 1 : 0);
+ if (mHasClientContext) {
+ parcel.writeParcelable(mUri, flags);
+ parcel.writeBundle(mExtras);
+ }
+ parcel.writeParcelable(mComponentName, flags);
+ if (mComponentName != null) {
+ parcel.writeInt(mTaskId);
+ parcel.writeInt(mDisplayId);
+ parcel.writeInt(mFlags);
+ }
+ }
+
+ public static final Parcelable.Creator<ContentCaptureContext> CREATOR =
+ new Parcelable.Creator<ContentCaptureContext>() {
+
+ @Override
+ public ContentCaptureContext createFromParcel(Parcel parcel) {
+ final boolean hasClientContext = parcel.readInt() == 1;
+
+ final ContentCaptureContext clientContext;
+ if (hasClientContext) {
+ final Builder builder = new Builder();
+ final Uri uri = parcel.readParcelable(null);
+ final Bundle extras = parcel.readBundle();
+ if (uri != null) builder.setUri(uri);
+ if (extras != null) builder.setExtras(extras);
+ // Must reconstruct the client context using the Builder API
+ clientContext = new ContentCaptureContext(builder);
+ } else {
+ clientContext = null;
+ }
+ final ComponentName componentName = parcel.readParcelable(null);
+ if (componentName == null) {
+ // Client-state only
+ return clientContext;
+ } else {
+ final int taskId = parcel.readInt();
+ final int displayId = parcel.readInt();
+ final int flags = parcel.readInt();
+ return new ContentCaptureContext(clientContext, componentName, taskId,
+ displayId, flags);
+ }
+ }
+
+ @Override
+ public ContentCaptureContext[] newArray(int size) {
+ return new ContentCaptureContext[size];
+ }
+ };
+}
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index 66fa530758e7..5d8fe5f51464 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -21,7 +21,6 @@ import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.SystemClock;
import android.view.autofill.AutofillId;
import com.android.internal.util.Preconditions;
@@ -41,54 +40,18 @@ public final class ContentCaptureEvent implements Parcelable {
public static final int TYPE_ACTIVITY_CREATED = -1;
/**
- * Called when the activity is started.
- *
- * @deprecated - TODO(b/111276913): we should abstract the Activity lifecycle concepts into
- * something related to a session and/or domain.
- */
- @Deprecated
- public static final int TYPE_ACTIVITY_STARTED = 1;
-
- /**
- * Called when the activity is resumed.
- *
- * @deprecated - TODO(b/111276913): we should abstract the Activity lifecycle concepts into
- * something related to a session and/or domain.
- */
- @Deprecated
- public static final int TYPE_ACTIVITY_RESUMED = 2;
-
- /**
- * Called when the activity is paused.
- *
- * @deprecated - TODO(b/111276913): we should abstract the Activity lifecycle concepts into
- * something related to a session and/or domain.
- */
- @Deprecated
- public static final int TYPE_ACTIVITY_PAUSED = 3;
-
- /**
- * Called when the activity is stopped.
- *
- * @deprecated - TODO(b/111276913): we should abstract the Activity lifecycle concepts into
- * something related to a session and/or domain.
- */
- @Deprecated
- public static final int TYPE_ACTIVITY_STOPPED = 4;
-
- /**
* Called when a node has been added to the screen and is visible to the user.
*
* <p>The metadata of the node is available through {@link #getViewNode()}.
*/
- public static final int TYPE_VIEW_APPEARED = 5;
+ public static final int TYPE_VIEW_APPEARED = 1;
/**
* Called when a node has been removed from the screen and is not visible to the user anymore.
*
* <p>The id of the node is available through {@link #getId()}.
*/
- public static final int TYPE_VIEW_DISAPPEARED = 6;
+ public static final int TYPE_VIEW_DISAPPEARED = 2;
/**
* Called when the text of a node has been changed.
@@ -96,16 +59,12 @@ public final class ContentCaptureEvent implements Parcelable {
* <p>The id of the node is available through {@link #getId()}, and the new text is
* available through {@link #getText()}.
*/
- public static final int TYPE_VIEW_TEXT_CHANGED = 7;
+ public static final int TYPE_VIEW_TEXT_CHANGED = 3;
// TODO(b/111276913): add event to indicate when FLAG_SECURE was changed?
/** @hide */
@IntDef(prefix = { "TYPE_" }, value = {
- TYPE_ACTIVITY_STARTED,
- TYPE_ACTIVITY_PAUSED,
- TYPE_ACTIVITY_RESUMED,
- TYPE_ACTIVITY_STOPPED,
TYPE_VIEW_APPEARED,
TYPE_VIEW_DISAPPEARED,
TYPE_VIEW_TEXT_CHANGED
@@ -130,7 +89,7 @@ public final class ContentCaptureEvent implements Parcelable {
/** @hide */
public ContentCaptureEvent(int type, int flags) {
- this(type, SystemClock.uptimeMillis(), flags);
+ this(type, System.currentTimeMillis(), flags);
}
/** @hide */
@@ -159,9 +118,7 @@ public final class ContentCaptureEvent implements Parcelable {
/**
* Gets the type of the event.
*
- * @return one of {@link #TYPE_ACTIVITY_STARTED}, {@link #TYPE_ACTIVITY_RESUMED},
- * {@link #TYPE_ACTIVITY_PAUSED}, {@link #TYPE_ACTIVITY_STOPPED},
- * {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED},
+ * @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED},
* or {@link #TYPE_VIEW_TEXT_CHANGED}.
*/
public @EventType int getType() {
@@ -169,7 +126,7 @@ public final class ContentCaptureEvent implements Parcelable {
}
/**
- * Gets when the event was generated, in ms.
+ * Gets when the event was generated, in millis since epoch.
*/
public long getEventTime() {
return mEventTime;
@@ -179,7 +136,7 @@ public final class ContentCaptureEvent implements Parcelable {
* Gets optional flags associated with the event.
*
* @return either {@code 0} or
- * {@link android.view.contentcapture.ContentCaptureManager#FLAG_USER_INPUT}.
+ * {@link android.view.contentcapture.ContentCaptureSession#FLAG_USER_INPUT}.
*/
public int getFlags() {
return mFlags;
@@ -295,14 +252,6 @@ public final class ContentCaptureEvent implements Parcelable {
/** @hide */
public static String getTypeAsString(@EventType int type) {
switch (type) {
- case TYPE_ACTIVITY_STARTED:
- return "ACTIVITY_STARTED";
- case TYPE_ACTIVITY_RESUMED:
- return "ACTIVITY_RESUMED";
- case TYPE_ACTIVITY_PAUSED:
- return "ACTIVITY_PAUSED";
- case TYPE_ACTIVITY_STOPPED:
- return "ACTIVITY_STOPPED";
case TYPE_VIEW_APPEARED:
return "VIEW_APPEARED";
case TYPE_VIEW_DISAPPEARED:
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index cc0264ac0bc9..7fbbfb775397 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -15,36 +15,20 @@
*/
package android.view.contentcapture;
-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 com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.ComponentName;
import android.content.Context;
-import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.SystemClock;
import android.util.Log;
-import android.util.TimeUtils;
import android.view.View;
-import android.view.ViewStructure;
-import android.view.autofill.AutofillId;
-import android.view.contentcapture.ContentCaptureEvent.EventType;
-import com.android.internal.os.IResultReceiver;
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/*
@@ -62,63 +46,11 @@ public final class ContentCaptureManager {
private static final String TAG = ContentCaptureManager.class.getSimpleName();
- // TODO(b/111276913): define a way to dynamically set them(for example, using settings?)
- private static final boolean VERBOSE = false;
- private static final boolean DEBUG = true; // STOPSHIP if not set to false
-
- /**
- * Used to indicate that a text change was caused by user input (for example, through IME).
- */
- //TODO(b/111276913): link to notifyTextChanged() method once available
- public static final int FLAG_USER_INPUT = 0x1;
-
- /**
- * Initial state, when there is no session.
- *
- * @hide
- */
- public static final int STATE_UNKNOWN = 0;
-
- /**
- * Service's startSession() was called, but server didn't confirm it was created yet.
- *
- * @hide
- */
- public static final int STATE_WAITING_FOR_SERVER = 1;
-
- /**
- * Session is active.
- *
- * @hide
- */
- public static final int STATE_ACTIVE = 2;
-
- /**
- * Session is disabled.
- *
- * @hide
- */
- public static final int STATE_DISABLED = 3;
-
- /**
- * Handler message used to flush the buffer.
- */
- private static final int MSG_FLUSH = 1;
-
-
private static final String BG_THREAD_NAME = "intel_svc_streamer_thread";
- /**
- * Maximum number of events that are buffered before sent to the app.
- */
- // TODO(b/111276913): use settings
- private static final int MAX_BUFFER_SIZE = 100;
-
- /**
- * Frequency the buffer is flushed if stale.
- */
- // TODO(b/111276913): use settings
- private static final int FLUSHING_FREQUENCY_MS = 5_000;
+ // TODO(b/121044306): define a way to dynamically set them(for example, using settings?)
+ static final boolean VERBOSE = false;
+ static final boolean DEBUG = true; // STOPSHIP if not set to false
@NonNull
private final AtomicBoolean mDisabled = new AtomicBoolean();
@@ -129,29 +61,12 @@ public final class ContentCaptureManager {
@Nullable
private final IContentCaptureManager mService;
- @Nullable
- private String mId;
-
- private int mState = STATE_UNKNOWN;
-
- @Nullable
- private IBinder mApplicationToken;
-
- @Nullable
- private ComponentName mComponentName;
-
- /**
- * List of events held to be sent as a batch.
- */
- @Nullable
- private ArrayList<ContentCaptureEvent> mEvents;
-
- // TODO(b/111276913): use UI Thread directly (as calls are one-way) or a shared thread / handler
+ // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
// held at the Application level
+ @NonNull
private final Handler mHandler;
- // Used just for debugging purposes (on dump)
- private long mNextFlush;
+ private ContentCaptureSession mMainSession;
/** @hide */
public ContentCaptureManager(@NonNull Context context,
@@ -161,290 +76,93 @@ public final class ContentCaptureManager {
Log.v(TAG, "Constructor for " + context.getPackageName());
}
mService = service;
- // TODO(b/111276913): use an existing bg thread instead...
+ // TODO(b/119220549): use an existing bg thread instead...
final HandlerThread bgThread = new HandlerThread(BG_THREAD_NAME);
bgThread.start();
mHandler = Handler.createAsync(bgThread.getLooper());
}
- /** @hide */
- public void onActivityCreated(@NonNull IBinder token, @NonNull ComponentName componentName) {
- if (!isContentCaptureEnabled()) return;
-
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleStartSession, this,
- token, componentName));
- }
-
- private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
- if (mState != STATE_UNKNOWN) {
- // TODO(b/111276913): revisit this scenario
- Log.w(TAG, "ignoring handleStartSession(" + token + ") while on state "
- + getStateAsString(mState));
- return;
- }
- mState = STATE_WAITING_FOR_SERVER;
- mId = UUID.randomUUID().toString();
- mApplicationToken = token;
- mComponentName = componentName;
-
- if (VERBOSE) {
- Log.v(TAG, "handleStartSession(): token=" + token + ", act="
- + getActivityDebugName() + ", id=" + mId);
- }
- final int flags = 0; // TODO(b/111276913): get proper flags
-
- try {
- mService.startSession(mContext.getUserId(), mApplicationToken, componentName,
- mId, flags, new IResultReceiver.Stub() {
- @Override
- public void send(int resultCode, Bundle resultData) {
- handleSessionStarted(resultCode);
- }
- });
- } catch (RemoteException e) {
- Log.w(TAG, "Error starting session for " + componentName.flattenToShortString() + ": "
- + e);
- }
- }
-
- private void handleSessionStarted(int resultCode) {
- mState = resultCode;
- mDisabled.set(mState == STATE_DISABLED);
- if (VERBOSE) {
- Log.v(TAG, "onActivityStarted() result: code=" + resultCode + ", id=" + mId
- + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get());
- }
- }
-
- private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
- if (mEvents == null) {
- if (VERBOSE) {
- Log.v(TAG, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
- }
- mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
- }
- mEvents.add(event);
-
- final int numberEvents = mEvents.size();
-
- // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
- // buffered (either total or per autofillid). For
- // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
- // "a" and "b" then send "abc".
- final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
-
- if (bufferEvent && !forceFlush) {
- handleScheduleFlush();
- return;
- }
-
- if (mState != STATE_ACTIVE) {
- // Callback from startSession hasn't been called yet - typically happens on system
- // apps that are started before the system service
- // TODO(b/111276913): try to ignore session while system is not ready / boot
- // not complete instead. Similarly, the manager service should return right away
- // when the user does not have a service set
- if (VERBOSE) {
- Log.v(TAG, "Closing session for " + getActivityDebugName()
- + " after " + numberEvents + " delayed events and state "
- + getStateAsString(mState));
- }
- handleResetState();
- // TODO(b/111276913): blacklist activity / use special flag to indicate that
- // when it's launched again
- return;
- }
-
- if (mId == null) {
- // Sanity check - should not happen
- Log.wtf(TAG, "null session id for " + getActivityDebugName());
- return;
- }
-
- handleForceFlush();
- }
-
- private void handleScheduleFlush() {
- if (mHandler.hasMessages(MSG_FLUSH)) {
- // "Renew" the flush message by removing the previous one
- mHandler.removeMessages(MSG_FLUSH);
- }
- mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
- if (VERBOSE) {
- Log.v(TAG, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
- }
- mHandler.sendMessageDelayed(
- obtainMessage(ContentCaptureManager::handleFlushIfNeeded, this).setWhat(MSG_FLUSH),
- FLUSHING_FREQUENCY_MS);
- }
-
- private void handleFlushIfNeeded() {
- if (mEvents.isEmpty()) {
- if (VERBOSE) Log.v(TAG, "Nothing to flush");
- return;
- }
- handleForceFlush();
- }
-
- private void handleForceFlush() {
- final int numberEvents = mEvents.size();
- try {
- if (DEBUG) {
- Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
- }
- mHandler.removeMessages(MSG_FLUSH);
- mService.sendEvents(mContext.getUserId(), mId, mEvents);
- // TODO(b/111276913): decide whether we should clear or set it to null, as each has
- // its own advantages: clearing will save extra allocations while the session is
- // active, while setting to null would save memory if there's no more event coming.
- mEvents.clear();
- } catch (RemoteException e) {
- Log.w(TAG, "Error sending " + numberEvents + " for " + getActivityDebugName()
- + ": " + e);
- }
+ @NonNull
+ private static Handler newHandler() {
+ // TODO(b/119220549): use an existing bg thread instead...
+ // TODO(b/119220549): use UI Thread directly (as calls are one-way) or an existing bgThread
+ // or a shared thread / handler held at the Application level
+ final HandlerThread bgThread = new HandlerThread(BG_THREAD_NAME);
+ bgThread.start();
+ return Handler.createAsync(bgThread.getLooper());
}
/**
- * Used for intermediate events (i.e, other than created and destroyed).
+ * Creates a new {@link ContentCaptureSession}.
*
- * @hide
+ * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info.
*/
- public void onActivityLifecycleEvent(@EventType int type) {
- if (!isContentCaptureEnabled()) return;
- if (VERBOSE) {
- Log.v(TAG, "onActivityLifecycleEvent() for " + getActivityDebugName()
- + ": " + ContentCaptureEvent.getTypeAsString(type));
- }
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleSendEvent, this,
- new ContentCaptureEvent(type), /* forceFlush= */ true));
- }
-
- /** @hide */
- public void onActivityDestroyed() {
- if (!isContentCaptureEnabled()) return;
-
- //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
- // id) and send it to the cache of batched commands
- if (VERBOSE) {
- Log.v(TAG, "onActivityDestroyed(): state=" + getStateAsString(mState)
- + ", mId=" + mId);
- }
-
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleFinishSession, this));
- }
-
- private void handleFinishSession() {
- //TODO(b/111276913): right now both the ContentEvents and lifecycle sessions are sent
- // to system_server, so it's ok to call both in sequence here. But once we split
- // them so the events are sent directly to the service, we need to make sure they're
- // sent in order.
- try {
- if (DEBUG) {
- Log.d(TAG, "Finishing session " + mId + " with "
- + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
- + getActivityDebugName());
- }
-
- mService.finishSession(mContext.getUserId(), mId, mEvents);
- } catch (RemoteException e) {
- Log.e(TAG, "Error finishing session " + mId + " for " + getActivityDebugName()
- + ": " + e);
- } finally {
- handleResetState();
- }
- }
-
- private void handleResetState() {
- mState = STATE_UNKNOWN;
- mId = null;
- mApplicationToken = null;
- mComponentName = null;
- mEvents = null;
- mHandler.removeMessages(MSG_FLUSH);
- }
-
- /**
- * Notifies the Intelligence Service that a node has been added to the view structure.
+ @NonNull
+ public ContentCaptureSession createContentCaptureSession(
+ @NonNull ContentCaptureContext context) {
+ if (DEBUG) Log.d(TAG, "createContentCaptureSession(): " + context);
+ // TODO(b/121033016): for now we're updating the main session, but we need instead:
+ // 1.Keep a list of sessions
+ // 2.Making sure the applicationToken and componentName passed by
+ // the activity is used on all of these sessions
+ // 3.We might also need to delay the start of all of these sessions until
+ // onActivityStarted() is called (and the main session is created).
+ // 4.Close (and delete) these sessions when onActivityStopped() is called.
+ // 5.Figure out whether each session will have its own mDisabled AtomicBoolean.
+ if (mMainSession == null) {
+ mMainSession = new ContentCaptureSession(mContext, mHandler, mService,
+ mDisabled, Preconditions.checkNotNull(context));
+ } else {
+ throw new IllegalStateException("Manager already has a session: " + mMainSession);
+ }
+ return mMainSession;
+ }
+
+ /**
+ * Gets the main session associated with the context.
*
- * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
- * automatically by the Android System for views that return {@code true} on
- * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}.
+ * <p>By default there's just one (associated with the activity lifecycle), but apps could
+ * explicitly add more using {@link #createContentCaptureSession(ContentCaptureContext)}.
*
- * @param node node that has been added.
+ * @hide
*/
- public void notifyViewAppeared(@NonNull ViewStructure node) {
- Preconditions.checkNotNull(node);
- if (!isContentCaptureEnabled()) return;
-
- if (!(node instanceof ViewNode.ViewStructureImpl)) {
- throw new IllegalArgumentException("Invalid node class: " + node.getClass());
+ @NonNull
+ public ContentCaptureSession getMainContentCaptureSession() {
+ // TODO(b/121033016): figure out how to manage the "default" session when it support
+ // multiple sessions (can't just be the first one, as it could be closed).
+ if (mMainSession == null) {
+ mMainSession = new ContentCaptureSession(mContext, mHandler, mService, mDisabled,
+ /* contentCaptureContext= */ null);
+ if (VERBOSE) {
+ Log.v(TAG, "getDefaultContentCaptureSession(): created " + mMainSession);
+ }
}
-
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleSendEvent, this,
- new ContentCaptureEvent(TYPE_VIEW_APPEARED)
- .setViewNode(((ViewNode.ViewStructureImpl) node).mNode),
- /* forceFlush= */ false));
- }
-
- /**
- * Notifies the Intelligence Service that a node has been removed from the view structure.
- *
- * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
- * automatically by the Android System for standard views.
- *
- * @param id id of the node that has been removed.
- */
- public void notifyViewDisappeared(@NonNull AutofillId id) {
- Preconditions.checkNotNull(id);
- if (!isContentCaptureEnabled()) return;
-
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleSendEvent, this,
- new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
- /* forceFlush= */ false));
+ return mMainSession;
}
- /**
- * Notifies the Intelligence Service that the value of a text node has been changed.
- *
- * @param id of the node.
- * @param text new text.
- * @param flags either {@code 0} or {@link #FLAG_USER_INPUT} when the value was explicitly
- * changed by the user (for example, through the keyboard).
- */
- public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
- int flags) {
- Preconditions.checkNotNull(id);
-
- if (!isContentCaptureEnabled()) return;
-
- mHandler.sendMessage(obtainMessage(ContentCaptureManager::handleSendEvent, this,
- new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
- .setText(text), /* forceFlush= */ false));
+ /** @hide */
+ public void onActivityStarted(@NonNull IBinder applicationToken,
+ @NonNull ComponentName activityComponent) {
+ // TODO(b/121033016): must start all sessions
+ getMainContentCaptureSession().start(applicationToken, activityComponent);
}
- /**
- * Creates a {@link ViewStructure} for a "standard" view.
- *
- * @hide
- */
- @NonNull
- public ViewStructure newViewStructure(@NonNull View view) {
- return new ViewNode.ViewStructureImpl(view);
+ /** @hide */
+ public void onActivityStopped() {
+ // TODO(b/121033016): must finish all sessions
+ getMainContentCaptureSession().destroy();
}
/**
- * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
- * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
+ * Flushes the content of all sessions.
*
- * @param parentId id of the virtual view parent (it can be obtained by calling
- * {@link ViewStructure#getAutofillId()} on the parent).
- * @param virtualId id of the virtual child, relative to the parent.
+ * <p>Typically called by {@code Activity} when it's paused / resumed.
*
- * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
+ * @hide
*/
- @NonNull
- public ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, int virtualId) {
- return new ViewNode.ViewStructureImpl(parentId, virtualId);
+ public void flush() {
+ // TODO(b/121033016): must flush all sessions
+ getMainContentCaptureSession().flush();
}
/**
@@ -453,7 +171,7 @@ public final class ContentCaptureManager {
*/
@Nullable
public ComponentName getServiceComponentName() {
- //TODO(b/111276913): implement
+ //TODO(b/121047489): implement
return null;
}
@@ -471,71 +189,35 @@ public final class ContentCaptureManager {
* it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}.
*/
public void setContentCaptureEnabled(boolean enabled) {
+ //TODO(b/111276913): implement (need to finish / disable all sessions)
+ }
+
+ /**
+ * Called by the ap to request the Content Capture service to remove user-data associated with
+ * some context.
+ *
+ * @param request object specifying what user data should be removed.
+ */
+ public void removeUserData(@NonNull UserDataRemovalRequest request) {
//TODO(b/111276913): implement
}
/** @hide */
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.println("ContentCaptureManager");
- final String prefix2 = prefix + " ";
- pw.print(prefix2); pw.print("mContext: "); pw.println(mContext);
- pw.print(prefix2); pw.print("user: "); pw.println(mContext.getUserId());
- if (mService != null) {
- pw.print(prefix2); pw.print("mService: "); pw.println(mService);
- }
- pw.print(prefix2); pw.print("mDisabled: "); pw.println(mDisabled.get());
- pw.print(prefix2); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
- if (mId != null) {
- pw.print(prefix2); pw.print("id: "); pw.println(mId);
- }
- pw.print(prefix2); pw.print("state: "); pw.print(mState); pw.print(" (");
- pw.print(getStateAsString(mState)); pw.println(")");
- if (mApplicationToken != null) {
- pw.print(prefix2); pw.print("app token: "); pw.println(mApplicationToken);
- }
- if (mComponentName != null) {
- pw.print(prefix2); pw.print("component name: ");
- pw.println(mComponentName.flattenToShortString());
- }
- if (mEvents != null && !mEvents.isEmpty()) {
- final int numberEvents = mEvents.size();
- pw.print(prefix2); pw.print("buffered events: "); pw.print(numberEvents);
- pw.print('/'); pw.println(MAX_BUFFER_SIZE);
- if (VERBOSE && numberEvents > 0) {
- final String prefix3 = prefix2 + " ";
- for (int i = 0; i < numberEvents; i++) {
- final ContentCaptureEvent event = mEvents.get(i);
- pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
- pw.println();
- }
- }
- pw.print(prefix2); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
- pw.print(prefix2); pw.print("next flush: ");
- TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
- }
- }
-
- /**
- * Gets a string that can be used to identify the activity on logging statements.
- */
- private String getActivityDebugName() {
- return mComponentName == null ? mContext.getPackageName()
- : mComponentName.flattenToShortString();
- }
- @NonNull
- private static String getStateAsString(int state) {
- switch (state) {
- case STATE_UNKNOWN:
- return "UNKNOWN";
- case STATE_WAITING_FOR_SERVER:
- return "WAITING_FOR_SERVER";
- case STATE_ACTIVE:
- return "ACTIVE";
- case STATE_DISABLED:
- return "DISABLED";
- default:
- return "INVALID:" + state;
+ pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled.get());
+ pw.print(prefix); pw.print("Context: "); pw.println(mContext);
+ pw.print(prefix); pw.print("User: "); pw.println(mContext.getUserId());
+ if (mService != null) {
+ pw.print(prefix); pw.print("Service: "); pw.println(mService);
+ }
+ if (mMainSession != null) {
+ final String prefix2 = prefix + " ";
+ pw.print(prefix); pw.println("Main session:");
+ mMainSession.dump(prefix2, pw);
+ } else {
+ pw.print(prefix); pw.println("No sessions");
}
}
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
new file mode 100644
index 000000000000..632955d335cf
--- /dev/null
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -0,0 +1,570 @@
+/*
+ * 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.view.contentcapture;
+
+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.ContentCaptureManager.DEBUG;
+import static android.view.contentcapture.ContentCaptureManager.VERBOSE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.TimeUtils;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Session used to notify a system-provided Content Capture service about events associated with
+ * views.
+ */
+public final class ContentCaptureSession implements AutoCloseable {
+
+ /*
+ * IMPLEMENTATION NOTICE:
+ *
+ * All methods in this class should return right away, or do the real work in a handler thread.
+ *
+ * Hence, the only field that must be thread-safe is mEnabled, which is called at the
+ * beginning of every method.
+ */
+
+ private static final String TAG = ContentCaptureSession.class.getSimpleName();
+
+ /**
+ * Used on {@link #notifyViewTextChanged(AutofillId, CharSequence, int)} to indicate that the
+ * thext change was caused by user input (for example, through IME).
+ */
+ public static final int FLAG_USER_INPUT = 0x1;
+
+ /**
+ * Initial state, when there is no session.
+ *
+ * @hide
+ */
+ public static final int STATE_UNKNOWN = 0;
+
+ /**
+ * Service's startSession() was called, but server didn't confirm it was created yet.
+ *
+ * @hide
+ */
+ public static final int STATE_WAITING_FOR_SERVER = 1;
+
+ /**
+ * Session is active.
+ *
+ * @hide
+ */
+ public static final int STATE_ACTIVE = 2;
+
+ /**
+ * Session is disabled.
+ *
+ * @hide
+ */
+ public static final int STATE_DISABLED = 3;
+
+ /**
+ * Handler message used to flush the buffer.
+ */
+ private static final int MSG_FLUSH = 1;
+
+ /**
+ * Maximum number of events that are buffered before sent to the app.
+ */
+ // TODO(b/121044064): use settings
+ private static final int MAX_BUFFER_SIZE = 100;
+
+ /**
+ * Frequency the buffer is flushed if stale.
+ */
+ // TODO(b/121044064): use settings
+ private static final int FLUSHING_FREQUENCY_MS = 5_000;
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ @NonNull
+ private final AtomicBoolean mDisabled;
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final Handler mHandler;
+
+ @Nullable
+ private final IContentCaptureManager mService;
+
+ @Nullable
+ private final String mId = UUID.randomUUID().toString();
+
+ private int mState = STATE_UNKNOWN;
+
+ @Nullable
+ private IBinder mApplicationToken;
+
+ @Nullable
+ private ComponentName mComponentName;
+
+ /**
+ * List of events held to be sent as a batch.
+ */
+ // TODO(b/111276913): once we support multiple sessions, we need to move the buffer of events
+ // to its own class so it's shared by all sessions
+ @Nullable
+ private ArrayList<ContentCaptureEvent> mEvents;
+
+ // Used just for debugging purposes (on dump)
+ private long mNextFlush;
+
+ // Lazily created on demand.
+ private ContentCaptureSessionId mContentCaptureSessionId;
+
+ /**
+ * {@link ContentCaptureContext} set by client, or {@code null} when it's the
+ * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
+ * context.
+ */
+ @Nullable
+ private final ContentCaptureContext mClientContext;
+
+ /** @hide */
+ protected ContentCaptureSession(@NonNull Context context, @NonNull Handler handler,
+ @Nullable IContentCaptureManager service, @NonNull AtomicBoolean disabled,
+ @Nullable ContentCaptureContext clientContext) {
+ mContext = context;
+ mHandler = handler;
+ mService = service;
+ mDisabled = disabled;
+ mClientContext = clientContext;
+ mCloseGuard.open("destroy");
+ }
+
+ /**
+ * Gets the id used to identify this session.
+ */
+ public ContentCaptureSessionId getContentCaptureSessionId() {
+ if (mContentCaptureSessionId == null) {
+ mContentCaptureSessionId = new ContentCaptureSessionId(mId);
+ }
+ return mContentCaptureSessionId;
+ }
+
+ /**
+ * Starts this session.
+ *
+ * @hide
+ */
+ void start(@NonNull IBinder applicationToken, @NonNull ComponentName activityComponent) {
+ if (!isContentCaptureEnabled()) return;
+
+ if (VERBOSE) {
+ Log.v(TAG, "start(): token=" + applicationToken + ", comp="
+ + ComponentName.flattenToShortString(activityComponent));
+ }
+
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleStartSession, this,
+ applicationToken, activityComponent));
+ }
+
+ /**
+ * Flushes the buffered events to the service.
+ *
+ * @hide
+ */
+ void flush() {
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleForceFlush, this));
+ }
+
+ /**
+ * Destroys this session, flushing out all pending notifications to the service.
+ *
+ * <p>Once destroyed, any new notification will be dropped.
+ */
+ public void destroy() {
+ //TODO(b/111276913): mark it as destroyed so other methods are ignored (and test on CTS)
+
+ if (!isContentCaptureEnabled()) return;
+
+ //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
+ // id) and send it to the cache of batched commands
+ if (VERBOSE) {
+ Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
+ }
+
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleDestroySession, this));
+ mCloseGuard.close();
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void handleStartSession(@NonNull IBinder token, @NonNull ComponentName componentName) {
+ if (mState != STATE_UNKNOWN) {
+ // TODO(b/111276913): revisit this scenario
+ Log.w(TAG, "ignoring handleStartSession(" + token + ") while on state "
+ + getStateAsString(mState));
+ return;
+ }
+ mState = STATE_WAITING_FOR_SERVER;
+ mApplicationToken = token;
+ mComponentName = componentName;
+
+ if (VERBOSE) {
+ Log.v(TAG, "handleStartSession(): token=" + token + ", act="
+ + getActivityDebugName() + ", id=" + mId);
+ }
+ final int flags = 0; // TODO(b/111276913): get proper flags
+
+ try {
+ mService.startSession(mContext.getUserId(), mApplicationToken, componentName,
+ mId, mClientContext, flags, new IResultReceiver.Stub() {
+ @Override
+ public void send(int resultCode, Bundle resultData) {
+ handleSessionStarted(resultCode);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error starting session for " + componentName.flattenToShortString() + ": "
+ + e);
+ }
+ }
+
+ private void handleSessionStarted(int resultCode) {
+ mState = resultCode;
+ mDisabled.set(mState == STATE_DISABLED);
+ if (VERBOSE) {
+ Log.v(TAG, "handleSessionStarted() result: code=" + resultCode + ", id=" + mId
+ + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get());
+ }
+ }
+
+ private void handleSendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ if (mEvents == null) {
+ if (VERBOSE) {
+ Log.v(TAG, "Creating buffer for " + MAX_BUFFER_SIZE + " events");
+ }
+ mEvents = new ArrayList<>(MAX_BUFFER_SIZE);
+ }
+ mEvents.add(event);
+
+ final int numberEvents = mEvents.size();
+
+ // TODO(b/120784831): need to optimize it so we buffer changes until a number of X are
+ // buffered (either total or per autofillid). For
+ // example, if the user typed "a", "b", "c" and the threshold is 3, we should buffer
+ // "a" and "b" then send "abc".
+ final boolean bufferEvent = numberEvents < MAX_BUFFER_SIZE;
+
+ if (bufferEvent && !forceFlush) {
+ handleScheduleFlush();
+ return;
+ }
+
+ if (mState != STATE_ACTIVE) {
+ // Callback from startSession hasn't been called yet - typically happens on system
+ // apps that are started before the system service
+ // TODO(b/111276913): try to ignore session while system is not ready / boot
+ // not complete instead. Similarly, the manager service should return right away
+ // when the user does not have a service set
+ if (VERBOSE) {
+ Log.v(TAG, "Closing session for " + getActivityDebugName()
+ + " after " + numberEvents + " delayed events and state "
+ + getStateAsString(mState));
+ }
+ handleResetState();
+ // TODO(b/111276913): blacklist activity / use special flag to indicate that
+ // when it's launched again
+ return;
+ }
+
+ handleForceFlush();
+ }
+
+ private void handleScheduleFlush() {
+ if (mHandler.hasMessages(MSG_FLUSH)) {
+ // "Renew" the flush message by removing the previous one
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+ mNextFlush = SystemClock.elapsedRealtime() + FLUSHING_FREQUENCY_MS;
+ if (VERBOSE) {
+ Log.v(TAG, "Scheduled to flush in " + FLUSHING_FREQUENCY_MS + "ms: " + mNextFlush);
+ }
+ mHandler.sendMessageDelayed(
+ obtainMessage(ContentCaptureSession::handleFlushIfNeeded, this).setWhat(MSG_FLUSH),
+ FLUSHING_FREQUENCY_MS);
+ }
+
+ private void handleFlushIfNeeded() {
+ if (mEvents.isEmpty()) {
+ if (VERBOSE) Log.v(TAG, "Nothing to flush");
+ return;
+ }
+ handleForceFlush();
+ }
+
+ private void handleForceFlush() {
+ if (mEvents == null) return;
+
+ final int numberEvents = mEvents.size();
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "Flushing " + numberEvents + " event(s) for " + getActivityDebugName());
+ }
+ mHandler.removeMessages(MSG_FLUSH);
+ mService.sendEvents(mContext.getUserId(), mId, mEvents);
+ // TODO(b/111276913): decide whether we should clear or set it to null, as each has
+ // its own advantages: clearing will save extra allocations while the session is
+ // active, while setting to null would save memory if there's no more event coming.
+ mEvents.clear();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error sending " + numberEvents + " for " + getActivityDebugName()
+ + ": " + e);
+ }
+ }
+
+ private void handleDestroySession() {
+ //TODO(b/111276913): right now both the ContentEvents and lifecycle sessions are sent
+ // to system_server, so it's ok to call both in sequence here. But once we split
+ // them so the events are sent directly to the service, we need to make sure they're
+ // sent in order.
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
+ + getActivityDebugName());
+ }
+
+ mService.finishSession(mContext.getUserId(), mId, mEvents);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error destroying session " + mId + " for " + getActivityDebugName()
+ + ": " + e);
+ } finally {
+ handleResetState();
+ }
+ }
+
+ // TODO(b/111276913): once we support multiple sessions, we might need to move some of these
+ // clearings out.
+ private void handleResetState() {
+ mState = STATE_UNKNOWN;
+ mContentCaptureSessionId = null;
+ mApplicationToken = null;
+ mComponentName = null;
+ mEvents = null;
+ mHandler.removeMessages(MSG_FLUSH);
+ }
+
+ /**
+ * Notifies the Content Capture Service that a node has been added to the view structure.
+ *
+ * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
+ * automatically by the Android System for views that return {@code true} on
+ * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}.
+ *
+ * @param node node that has been added.
+ */
+ public void notifyViewAppeared(@NonNull ViewStructure node) {
+ Preconditions.checkNotNull(node);
+ if (!isContentCaptureEnabled()) return;
+
+ if (!(node instanceof ViewNode.ViewStructureImpl)) {
+ throw new IllegalArgumentException("Invalid node class: " + node.getClass());
+ }
+
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(TYPE_VIEW_APPEARED)
+ .setViewNode(((ViewNode.ViewStructureImpl) node).mNode),
+ /* forceFlush= */ false));
+ }
+
+ /**
+ * Notifies the Content Capture Service that a node has been removed from the view structure.
+ *
+ * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or
+ * automatically by the Android System for standard views.
+ *
+ * @param id id of the node that has been removed.
+ */
+ public void notifyViewDisappeared(@NonNull AutofillId id) {
+ Preconditions.checkNotNull(id);
+ if (!isContentCaptureEnabled()) return;
+
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(TYPE_VIEW_DISAPPEARED).setAutofillId(id),
+ /* forceFlush= */ false));
+ }
+
+ /**
+ * Notifies the Intelligence Service that the value of a text node has been changed.
+ *
+ * @param id of the node.
+ * @param text new text.
+ * @param flags either {@code 0} or {@link #FLAG_USER_INPUT} when the value was explicitly
+ * changed by the user (for example, through the keyboard).
+ */
+ public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text,
+ int flags) {
+ Preconditions.checkNotNull(id);
+
+ if (!isContentCaptureEnabled()) return;
+
+ mHandler.sendMessage(obtainMessage(ContentCaptureSession::handleSendEvent, this,
+ new ContentCaptureEvent(TYPE_VIEW_TEXT_CHANGED, flags).setAutofillId(id)
+ .setText(text), /* forceFlush= */ false));
+ }
+
+ /**
+ * Creates a {@link ViewStructure} for a "standard" view.
+ *
+ * @hide
+ */
+ @NonNull
+ public ViewStructure newViewStructure(@NonNull View view) {
+ return new ViewNode.ViewStructureImpl(view);
+ }
+
+ /**
+ * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
+ * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
+ *
+ * @param parentId id of the virtual view parent (it can be obtained by calling
+ * {@link ViewStructure#getAutofillId()} on the parent).
+ * @param virtualId id of the virtual child, relative to the parent.
+ *
+ * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
+ *
+ * @hide
+ */
+ @NonNull
+ public ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, int virtualId) {
+ return new ViewNode.ViewStructureImpl(parentId, virtualId);
+ }
+
+ private boolean isContentCaptureEnabled() {
+ return mService != null && !mDisabled.get();
+ }
+
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.println(mId);
+ pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
+ pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
+ if (mService != null) {
+ pw.print(prefix); pw.print("mService: "); pw.println(mService);
+ }
+ if (mClientContext != null) {
+ // NOTE: we don't dump clientContent because it could have PII
+ pw.print(prefix); pw.println("hasClientContext");
+
+ }
+ pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
+ pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
+ if (mContentCaptureSessionId != null) {
+ pw.print(prefix); pw.print("public id: "); pw.println(mContentCaptureSessionId);
+ }
+ pw.print(prefix); pw.print("state: "); pw.print(mState); pw.print(" (");
+ pw.print(getStateAsString(mState)); pw.println(")");
+ if (mApplicationToken != null) {
+ pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
+ }
+ if (mComponentName != null) {
+ pw.print(prefix); pw.print("component name: ");
+ pw.println(mComponentName.flattenToShortString());
+ }
+ if (mEvents != null && !mEvents.isEmpty()) {
+ final int numberEvents = mEvents.size();
+ pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
+ pw.print('/'); pw.println(MAX_BUFFER_SIZE);
+ if (VERBOSE && numberEvents > 0) {
+ final String prefix3 = prefix + " ";
+ for (int i = 0; i < numberEvents; i++) {
+ final ContentCaptureEvent event = mEvents.get(i);
+ pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
+ pw.println();
+ }
+ }
+ pw.print(prefix); pw.print("flush frequency: "); pw.println(FLUSHING_FREQUENCY_MS);
+ pw.print(prefix); pw.print("next flush: ");
+ TimeUtils.formatDuration(mNextFlush - SystemClock.elapsedRealtime(), pw); pw.println();
+ }
+ }
+
+ /**
+ * Gets a string that can be used to identify the activity on logging statements.
+ */
+ private String getActivityDebugName() {
+ return mComponentName == null ? mContext.getPackageName()
+ : mComponentName.flattenToShortString();
+ }
+
+ @Override
+ public String toString() {
+ return mId;
+ }
+
+ @NonNull
+ private static String getStateAsString(int state) {
+ switch (state) {
+ case STATE_UNKNOWN:
+ return "UNKNOWN";
+ case STATE_WAITING_FOR_SERVER:
+ return "WAITING_FOR_SERVER";
+ case STATE_ACTIVE:
+ return "ACTIVE";
+ case STATE_DISABLED:
+ return "DISABLED";
+ default:
+ return "INVALID:" + state;
+ }
+ }
+}
diff --git a/core/java/android/service/contentcapture/InteractionSessionId.java b/core/java/android/view/contentcapture/ContentCaptureSessionId.java
index 84119472ca32..d7f9fcc4f7af 100644
--- a/core/java/android/service/contentcapture/InteractionSessionId.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSessionId.java
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package android.service.contentcapture;
+package android.view.contentcapture;
import android.annotation.NonNull;
-import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -25,11 +24,8 @@ import java.io.PrintWriter;
/**
* Identifier for a Content Capture session.
- *
- * @hide
*/
-@SystemApi
-public final class InteractionSessionId implements Parcelable {
+public final class ContentCaptureSessionId implements Parcelable {
private final @NonNull String mValue;
@@ -40,7 +36,7 @@ public final class InteractionSessionId implements Parcelable {
*
* @hide
*/
- public InteractionSessionId(@NonNull String value) {
+ public ContentCaptureSessionId(@NonNull String value) {
mValue = value;
}
@@ -64,7 +60,7 @@ public final class InteractionSessionId implements Parcelable {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
- final InteractionSessionId other = (InteractionSessionId) obj;
+ final ContentCaptureSessionId other = (ContentCaptureSessionId) obj;
if (mValue == null) {
if (other.mValue != null) return false;
} else if (!mValue.equals(other.mValue)) {
@@ -100,17 +96,17 @@ public final class InteractionSessionId implements Parcelable {
parcel.writeString(mValue);
}
- public static final Parcelable.Creator<InteractionSessionId> CREATOR =
- new Parcelable.Creator<InteractionSessionId>() {
+ public static final Parcelable.Creator<ContentCaptureSessionId> CREATOR =
+ new Parcelable.Creator<ContentCaptureSessionId>() {
@Override
- public InteractionSessionId createFromParcel(Parcel parcel) {
- return new InteractionSessionId(parcel.readString());
+ public ContentCaptureSessionId createFromParcel(Parcel parcel) {
+ return new ContentCaptureSessionId(parcel.readString());
}
@Override
- public InteractionSessionId[] newArray(int size) {
- return new InteractionSessionId[size];
+ public ContentCaptureSessionId[] newArray(int size) {
+ return new ContentCaptureSessionId[size];
}
};
}
diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
index 8704dad2ea08..2002c5c1fc9a 100644
--- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl
+++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl
@@ -17,6 +17,7 @@
package android.view.contentcapture;
import android.content.ComponentName;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.os.IBinder;
@@ -29,7 +30,8 @@ import java.util.List;
*/
oneway interface IContentCaptureManager {
void startSession(int userId, IBinder activityToken, in ComponentName componentName,
- String sessionId, int flags, in IResultReceiver result);
+ String sessionId, in ContentCaptureContext clientContext, int flags,
+ in IResultReceiver result);
void finishSession(int userId, String sessionId, in List<ContentCaptureEvent> events);
void sendEvents(int userId, in String sessionId, in List<ContentCaptureEvent> events);
}
diff --git a/core/java/android/view/contentcapture/UserDataRemovalRequest.java b/core/java/android/view/contentcapture/UserDataRemovalRequest.java
new file mode 100644
index 000000000000..0261b70825a5
--- /dev/null
+++ b/core/java/android/view/contentcapture/UserDataRemovalRequest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.view.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+
+/**
+ * Class used by apps to request the Content Capture service to remove user-data associated with
+ * some context.
+ */
+public final class UserDataRemovalRequest implements Parcelable {
+
+ private UserDataRemovalRequest(Builder builder) {
+ // TODO(b/111276913): implement
+ }
+
+ /**
+ * Gets the name of the app that's making the request.
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public String getPackageName() {
+ // TODO(b/111276913): implement
+ // TODO(b/111276913): make sure it's set on system_service so it cannot be faked by app
+ return null;
+ }
+
+ /**
+ * Checks if app is requesting to remove all user data associated with its package.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isForEverything() {
+ // TODO(b/111276913): implement
+ return false;
+ }
+
+ /**
+ * Gets the list of {@code Uri}s the apps is requesting to remove.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public List<UriRequest> getUriRequests() {
+ // TODO(b/111276913): implement
+ return null;
+ }
+
+ /**
+ * Builder for {@link UserDataRemovalRequest} objects.
+ */
+ public static final class Builder {
+
+ /**
+ * Requests servive to remove all user data associated with the app's package.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder forEverything() {
+ // TODO(b/111276913): implement
+ return this;
+ }
+
+ /**
+ * Request service to remove data associated with a given {@link Uri}.
+ *
+ * @param uri URI being requested to be removed.
+ * @param recursive whether it should remove the data associated with just the URI or its
+ * tree of descendants.
+ *
+ * @return this builder
+ */
+ public Builder addUri(@NonNull Uri uri, boolean recursive) {
+ // TODO(b/111276913): implement
+ return this;
+ }
+
+ /**
+ * Builds the {@link UserDataRemovalRequest}.
+ */
+ @NonNull
+ public UserDataRemovalRequest build() {
+ // TODO(b/111276913): implement / unit test / check built / document exceptions
+ return null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ // TODO(b/111276913): implement
+ }
+
+ public static final Parcelable.Creator<UserDataRemovalRequest> CREATOR =
+ new Parcelable.Creator<UserDataRemovalRequest>() {
+
+ @Override
+ public UserDataRemovalRequest createFromParcel(Parcel parcel) {
+ // TODO(b/111276913): implement
+ return null;
+ }
+
+ @Override
+ public UserDataRemovalRequest[] newArray(int size) {
+ return new UserDataRemovalRequest[size];
+ }
+ };
+
+ /**
+ * Representation of a request to remove data associated with an {@link Uri}.
+ * @hide
+ */
+ @SystemApi
+ public final class UriRequest {
+ private final @NonNull Uri mUri;
+ private final boolean mRecursive;
+
+ private UriRequest(@NonNull Uri uri, boolean recursive) {
+ this.mUri = uri;
+ this.mRecursive = recursive;
+ }
+
+ /**
+ * Gets the URI per se.
+ */
+ @NonNull
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Checks whether the request is to remove just the data associated with the URI per se, or
+ * also its descendants.
+ */
+ @NonNull
+ public boolean isRecursive() {
+ return mRecursive;
+ }
+ }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90da81276ba3..f30212af111d 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -158,6 +158,7 @@ import android.view.animation.AnimationUtils;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
@@ -10216,12 +10217,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ // TODO(b/121045053): should use a flag / boolean to keep status of SHOWN / HIDDEN instead
+ // of using isLaidout(), so it's not called in cases where it's laid out but a
+ // notifyAppeared was not sent.
+
// ContentCapture
if (isLaidOut() && isImportantForContentCapture() && isTextEditable()) {
final ContentCaptureManager cm = mContext.getSystemService(ContentCaptureManager.class);
if (cm != null && cm.isContentCaptureEnabled()) {
- // TODO(b/111276913): pass flags when edited by user / add CTS test
- cm.notifyViewTextChanged(getAutofillId(), getText(), /* flags= */ 0);
+ final ContentCaptureSession session = getContentCaptureSession();
+ if (session != null) {
+ // TODO(b/111276913): pass flags when edited by user / add CTS test
+ session.notifyViewTextChanged(getAutofillId(), getText(), /* flags= */ 0);
+ }
}
}
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 872fe4229479..10e713d9dabe 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -33,6 +33,7 @@ import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.IContentCaptureManager;
@@ -165,7 +166,8 @@ public final class ContentCaptureManagerService extends
@Override
public void startSession(@UserIdInt int userId, @NonNull IBinder activityToken,
@NonNull ComponentName componentName, @NonNull String sessionId,
- int flags, @NonNull IResultReceiver result) {
+ @Nullable ContentCaptureContext clientContext, int flags,
+ @NonNull IResultReceiver result) {
Preconditions.checkNotNull(activityToken);
Preconditions.checkNotNull(componentName);
Preconditions.checkNotNull(sessionId);
@@ -180,7 +182,7 @@ public final class ContentCaptureManagerService extends
synchronized (mLock) {
final ContentCapturePerUserService service = getServiceForUserLocked(userId);
service.startSessionLocked(activityToken, componentName, taskId, displayId,
- sessionId, flags, mAllowInstantService, result);
+ sessionId, clientContext, flags, mAllowInstantService, result);
}
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index aa171f4a0818..81309122d0ca 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -37,8 +37,9 @@ import android.os.RemoteException;
import android.service.contentcapture.SnapshotData;
import android.util.ArrayMap;
import android.util.Slog;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
-import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
@@ -59,7 +60,7 @@ final class ContentCapturePerUserService
private static final String TAG = ContentCaptureManagerService.class.getSimpleName();
@GuardedBy("mLock")
- private final ArrayMap<String, ContentCaptureSession> mSessions =
+ private final ArrayMap<String, ContentCaptureServerSession> mSessions =
new ArrayMap<>();
// TODO(b/111276913): add mechanism to prune stale sessions, similar to Autofill's
@@ -113,10 +114,10 @@ final class ContentCapturePerUserService
@GuardedBy("mLock")
public void startSessionLocked(@NonNull IBinder activityToken,
@NonNull ComponentName componentName, int taskId, int displayId,
- @NonNull String sessionId, int flags, boolean bindInstantServiceAllowed,
- @NonNull IResultReceiver resultReceiver) {
+ @NonNull String sessionId, @Nullable ContentCaptureContext clientContext,
+ int flags, boolean bindInstantServiceAllowed, @NonNull IResultReceiver resultReceiver) {
if (!isEnabledLocked()) {
- sendToClient(resultReceiver, ContentCaptureManager.STATE_DISABLED);
+ sendToClient(resultReceiver, ContentCaptureSession.STATE_DISABLED);
return;
}
final ComponentName serviceComponentName = getServiceComponentName();
@@ -130,7 +131,7 @@ final class ContentCapturePerUserService
return;
}
- ContentCaptureSession session = mSessions.get(sessionId);
+ ContentCaptureServerSession session = mSessions.get(sessionId);
if (session != null) {
if (mMaster.debug) {
Slog.d(TAG, "startSession(): reusing session " + sessionId + " for "
@@ -139,20 +140,20 @@ final class ContentCapturePerUserService
// TODO(b/111276913): check if local ids match and decide what to do if they don't
// TODO(b/111276913): should we call session.notifySessionStartedLocked() again??
// if not, move notifySessionStartedLocked() into session constructor
- sendToClient(resultReceiver, ContentCaptureManager.STATE_ACTIVE);
+ sendToClient(resultReceiver, ContentCaptureSession.STATE_ACTIVE);
return;
}
- session = new ContentCaptureSession(getContext(), mUserId, mLock, activityToken,
- this, serviceComponentName, componentName, taskId, displayId, sessionId, flags,
- bindInstantServiceAllowed, mMaster.verbose);
+ session = new ContentCaptureServerSession(getContext(), mUserId, mLock, activityToken,
+ this, serviceComponentName, componentName, taskId, displayId, sessionId,
+ clientContext, flags, bindInstantServiceAllowed, mMaster.verbose);
if (mMaster.verbose) {
Slog.v(TAG, "startSession(): new session for " + componentName + " and id "
+ sessionId);
}
mSessions.put(sessionId, session);
session.notifySessionStartedLocked();
- sendToClient(resultReceiver, ContentCaptureManager.STATE_ACTIVE);
+ sendToClient(resultReceiver, ContentCaptureSession.STATE_ACTIVE);
}
// TODO(b/111276913): log metrics
@@ -163,7 +164,7 @@ final class ContentCapturePerUserService
return;
}
- final ContentCaptureSession session = mSessions.get(sessionId);
+ final ContentCaptureServerSession session = mSessions.get(sessionId);
if (session == null) {
if (mMaster.debug) {
Slog.d(TAG, "finishSession(): no session with id" + sessionId);
@@ -194,7 +195,7 @@ final class ContentCapturePerUserService
if (!isEnabledLocked()) {
return;
}
- final ContentCaptureSession session = mSessions.get(sessionId);
+ final ContentCaptureServerSession session = mSessions.get(sessionId);
if (session == null) {
if (mMaster.verbose) {
Slog.v(TAG, "sendEvents(): no session for " + sessionId);
@@ -212,7 +213,7 @@ final class ContentCapturePerUserService
@NonNull Bundle data) {
final String id = getSessionId(activityToken);
if (id != null) {
- final ContentCaptureSession session = mSessions.get(id);
+ final ContentCaptureServerSession session = mSessions.get(id);
final Bundle assistData = data.getBundle(ASSIST_KEY_DATA);
final AssistStructure assistStructure = data.getParcelable(ASSIST_KEY_STRUCTURE);
final AssistContent assistContent = data.getParcelable(ASSIST_KEY_CONTENT);
@@ -237,9 +238,9 @@ final class ContentCapturePerUserService
}
@GuardedBy("mLock")
- private ContentCaptureSession getSession(@NonNull IBinder activityToken) {
+ private ContentCaptureServerSession getSession(@NonNull IBinder activityToken) {
for (int i = 0; i < mSessions.size(); i++) {
- final ContentCaptureSession session = mSessions.valueAt(i);
+ final ContentCaptureServerSession session = mSessions.valueAt(i);
if (session.mActivityToken.equals(activityToken)) {
return session;
}
@@ -262,7 +263,7 @@ final class ContentCapturePerUserService
void destroySessionsLocked() {
final int numSessions = mSessions.size();
for (int i = 0; i < numSessions; i++) {
- final ContentCaptureSession session = mSessions.valueAt(i);
+ final ContentCaptureServerSession session = mSessions.valueAt(i);
session.destroyLocked(true);
}
mSessions.clear();
@@ -272,7 +273,7 @@ final class ContentCapturePerUserService
void listSessionsLocked(ArrayList<String> output) {
final int numSessions = mSessions.size();
for (int i = 0; i < numSessions; i++) {
- final ContentCaptureSession session = mSessions.valueAt(i);
+ final ContentCaptureServerSession session = mSessions.valueAt(i);
output.add(session.toShortString());
}
}
@@ -288,7 +289,7 @@ final class ContentCapturePerUserService
final String prefix2 = prefix + " ";
for (int i = 0; i < size; i++) {
pw.print(prefix); pw.print("session@"); pw.println(i);
- final ContentCaptureSession session = mSessions.valueAt(i);
+ final ContentCaptureServerSession session = mSessions.valueAt(i);
session.dumpLocked(prefix2, pw);
}
}
@@ -300,7 +301,7 @@ final class ContentCapturePerUserService
@GuardedBy("mLock")
private String getSessionId(@NonNull IBinder activityToken) {
for (int i = 0; i < mSessions.size(); i++) {
- ContentCaptureSession session = mSessions.valueAt(i);
+ ContentCaptureServerSession session = mSessions.valueAt(i);
if (session.isActivitySession(activityToken)) {
return mSessions.keyAt(i);
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
index a4012d5faa8a..181a2daa2467 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureSession.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
@@ -16,15 +16,16 @@
package com.android.server.contentcapture;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
import android.service.contentcapture.ContentCaptureService;
-import android.service.contentcapture.InteractionContext;
-import android.service.contentcapture.InteractionSessionId;
import android.service.contentcapture.SnapshotData;
import android.util.Slog;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ContentCaptureSessionId;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
@@ -33,21 +34,22 @@ import com.android.server.contentcapture.RemoteContentCaptureService.ContentCapt
import java.io.PrintWriter;
import java.util.List;
-final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
+final class ContentCaptureServerSession implements ContentCaptureServiceCallbacks {
- private static final String TAG = "ContentCaptureSession";
+ private static final String TAG = ContentCaptureServerSession.class.getSimpleName();
private final Object mLock;
final IBinder mActivityToken;
private final ContentCapturePerUserService mService;
private final RemoteContentCaptureService mRemoteService;
- private final InteractionContext mInterationContext;
+ private final ContentCaptureContext mContentCaptureContext;
private final String mId;
- ContentCaptureSession(@NonNull Context context, int userId, @NonNull Object lock,
+ ContentCaptureServerSession(@NonNull Context context, int userId, @NonNull Object lock,
@NonNull IBinder activityToken, @NonNull ContentCapturePerUserService service,
@NonNull ComponentName serviceComponentName, @NonNull ComponentName appComponentName,
- int taskId, int displayId, @NonNull String sessionId, int flags,
+ int taskId, int displayId, @NonNull String sessionId,
+ @Nullable ContentCaptureContext clientContext, int flags,
boolean bindInstantServiceAllowed, boolean verbose) {
mLock = lock;
mActivityToken = activityToken;
@@ -56,7 +58,8 @@ final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
mRemoteService = new RemoteContentCaptureService(context,
ContentCaptureService.SERVICE_INTERFACE, serviceComponentName, userId, this,
bindInstantServiceAllowed, verbose);
- mInterationContext = new InteractionContext(appComponentName, taskId, displayId, flags);
+ mContentCaptureContext = new ContentCaptureContext(clientContext, appComponentName, taskId,
+ displayId, flags);
}
/**
@@ -71,7 +74,7 @@ final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
*/
@GuardedBy("mLock")
public void notifySessionStartedLocked() {
- mRemoteService.onSessionLifecycleRequest(mInterationContext, mId);
+ mRemoteService.onSessionLifecycleRequest(mContentCaptureContext, mId);
}
/**
@@ -93,7 +96,7 @@ final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
* Cleans up the session and removes it from the service.
*
* @param notifyRemoteService whether it should trigger a {@link
- * ContentCaptureService#onDestroyInteractionSession(InteractionSessionId)}
+ * ContentCaptureService#onDestroyContentCaptureSession(ContentCaptureSessionId)}
* request.
*/
@GuardedBy("mLock")
@@ -109,7 +112,7 @@ final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
* Cleans up the session, but not removes it from the service.
*
* @param notifyRemoteService whether it should trigger a {@link
- * ContentCaptureService#onDestroyInteractionSession(InteractionSessionId)}
+ * ContentCaptureService#onDestroyContentCaptureSession(ContentCaptureSessionId)}
* request.
*/
@GuardedBy("mLock")
@@ -137,7 +140,7 @@ final class ContentCaptureSession implements ContentCaptureServiceCallbacks {
@GuardedBy("mLock")
public void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.print(mId); pw.println();
- pw.print(prefix); pw.print("context: "); mInterationContext.dump(pw); pw.println();
+ pw.print(prefix); pw.print("context: "); mContentCaptureContext.dump(pw); pw.println();
pw.print(prefix); pw.print("activity token: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("has autofill callback: ");
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
index 33b6c8d5eec4..b4edf7e60bc4 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
@@ -22,9 +22,9 @@ import android.content.Context;
import android.os.IBinder;
import android.service.contentcapture.ContentCaptureEventsRequest;
import android.service.contentcapture.IContentCaptureService;
-import android.service.contentcapture.InteractionContext;
import android.service.contentcapture.SnapshotData;
import android.text.format.DateUtils;
+import android.view.contentcapture.ContentCaptureContext;
import android.view.contentcapture.ContentCaptureEvent;
import com.android.server.infra.AbstractMultiplePendingRequestsRemoteService;
@@ -66,17 +66,17 @@ final class RemoteContentCaptureService
}
/**
- * Called by {@link ContentCaptureSession} to generate a call to the
+ * Called by {@link ContentCaptureServerSession} to generate a call to the
* {@link RemoteContentCaptureService} to indicate the session was created (when {@code context}
* is not {@code null} or destroyed (when {@code context} is {@code null}).
*/
- public void onSessionLifecycleRequest(@Nullable InteractionContext context,
+ public void onSessionLifecycleRequest(@Nullable ContentCaptureContext context,
@NonNull String sessionId) {
scheduleAsyncRequest((s) -> s.onSessionLifecycle(context, sessionId));
}
/**
- * Called by {@link ContentCaptureSession} to send a batch of events to the service.
+ * Called by {@link ContentCaptureServerSession} to send a batch of events to the service.
*/
public void onContentCaptureEventsRequest(@NonNull String sessionId,
@NonNull List<ContentCaptureEvent> events) {
@@ -85,7 +85,7 @@ final class RemoteContentCaptureService
}
/**
- * Called by {@link ContentCaptureSession} to send snapshot data to the service.
+ * Called by {@link ContentCaptureServerSession} to send snapshot data to the service.
*/
public void onActivitySnapshotRequest(@NonNull String sessionId,
@NonNull SnapshotData snapshotData) {
@@ -94,8 +94,8 @@ final class RemoteContentCaptureService
public interface ContentCaptureServiceCallbacks
extends VultureCallback<RemoteContentCaptureService> {
- // NOTE: so far we don't need to notify the callback implementation (an inner class on
- // AutofillManagerServiceImpl) of the request results (success, timeouts, etc..), so this
+ // NOTE: so far we don't need to notify the callback implementation
+ // (ContentCaptureServerSession) of the request results (success, timeouts, etc..), so this
// callback interface is empty.
}
}