diff options
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. } } |