Add new ContentCapture APIs to let apps change the ContentCaptureContext.

Test: atest CtsContentCaptureServiceTestCases:android.contentcaptureservice.cts.LoginActivityTest#testSimpleLifecycle_changeContextOnCreate \
   CtsContentCaptureServiceTestCases:android.contentcaptureservice.cts.LoginActivityTest#testSimpleLifecycle_changeContextAfterCreate
Test: atest FrameworksCoreTests:android.view.contentcapture.ContentCaptureEventTest

Bug: 124266664

Change-Id: I0348e81e1b2bac01363cf615d2ab32e5bab8aee1
This commit is contained in:
Felipe Leme
2019-02-11 17:50:17 -08:00
parent 7c2f57169c
commit 4eecbe6e3c
12 changed files with 146 additions and 40 deletions

View File

@@ -53462,6 +53462,7 @@ package android.view.contentcapture {
method public void close();
method @NonNull public final android.view.contentcapture.ContentCaptureSession createContentCaptureSession(@NonNull android.view.contentcapture.ContentCaptureContext);
method public final void destroy();
method @Nullable public final android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
method public final android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId();
method @NonNull public android.view.autofill.AutofillId newAutofillId(@NonNull android.view.autofill.AutofillId, long);
method @NonNull public final android.view.ViewStructure newVirtualViewStructure(@NonNull android.view.autofill.AutofillId, long);
@@ -53469,6 +53470,7 @@ package android.view.contentcapture {
method public final void notifyViewDisappeared(@NonNull android.view.autofill.AutofillId);
method public final void notifyViewTextChanged(@NonNull android.view.autofill.AutofillId, @Nullable CharSequence);
method public final void notifyViewsDisappeared(@NonNull android.view.autofill.AutofillId, @NonNull long[]);
method public final void setContentCaptureContext(@Nullable android.view.contentcapture.ContentCaptureContext);
}
public final class ContentCaptureSessionId implements android.os.Parcelable {

View File

@@ -9317,6 +9317,7 @@ package android.view.contentcapture {
public final class ContentCaptureEvent implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
method public long getEventTime();
method @Nullable public android.view.autofill.AutofillId getId();
method @Nullable public java.util.List<android.view.autofill.AutofillId> getIds();
@@ -9325,6 +9326,7 @@ package android.view.contentcapture {
method @Nullable 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 int TYPE_CONTEXT_UPDATED = 6; // 0x6
field public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5; // 0x5
field public static final int TYPE_INITIAL_VIEW_TREE_APPEARING = 4; // 0x4
field public static final int TYPE_VIEW_APPEARED = 1; // 0x1

View File

@@ -2725,6 +2725,7 @@ package android.view.contentcapture {
public final class ContentCaptureEvent implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
method public long getEventTime();
method @Nullable public android.view.autofill.AutofillId getId();
method @Nullable public java.util.List<android.view.autofill.AutofillId> getIds();
@@ -2733,6 +2734,7 @@ package android.view.contentcapture {
method @Nullable 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 int TYPE_CONTEXT_UPDATED = 6; // 0x6
field public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5; // 0x5
field public static final int TYPE_INITIAL_VIEW_TREE_APPEARING = 4; // 0x4
field public static final int TYPE_VIEW_APPEARED = 1; // 0x1

View File

@@ -339,7 +339,7 @@ public abstract class ContentCaptureService extends Service {
}
switch (event.getType()) {
case ContentCaptureEvent.TYPE_SESSION_STARTED:
final ContentCaptureContext clientContext = event.getClientContext();
final ContentCaptureContext clientContext = event.getContentCaptureContext();
clientContext.setParentSessionId(event.getParentSessionId());
mSessionUids.put(sessionIdString, uid);
onCreateContentCaptureSession(clientContext, sessionId);
@@ -383,8 +383,8 @@ public abstract class ContentCaptureService extends Service {
}
final Integer rightUid = mSessionUids.get(sessionId);
if (rightUid == null) {
if (DEBUG) {
Log.d(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
if (VERBOSE) {
Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
+ ": " + mSessionUids);
}
// Just ignore, as the session could have been finished already

View File

@@ -9366,7 +9366,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* 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
* inherited by ancestors, default session or {@code null} if content capture is disabled for
* this view.
*/
@Nullable

View File

@@ -20,10 +20,6 @@ import android.annotation.Nullable;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
/**
* A session that is explicitly created by the app (and hence is a descendant of
* {@link MainContentCaptureSession}).
@@ -35,21 +31,11 @@ final class ChildContentCaptureSession extends ContentCaptureSession {
@NonNull
private final ContentCaptureSession mParent;
/**
* {@link ContentCaptureContext} set by client, or {@code null} when it's the
* {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the
* context.
*
* @hide
*/
@NonNull
private final ContentCaptureContext mClientContext;
/** @hide */
protected ChildContentCaptureSession(@NonNull ContentCaptureSession parent,
@NonNull ContentCaptureContext clientContext) {
super(clientContext);
mParent = parent;
mClientContext = Preconditions.checkNotNull(clientContext);
}
@Override
@@ -72,6 +58,11 @@ final class ChildContentCaptureSession extends ContentCaptureSession {
mParent.flush(reason);
}
@Override
public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
getMainCaptureSession().notifyContextUpdated(mId, context);
}
@Override
void onDestroy() {
getMainCaptureSession().notifyChildSessionFinished(mParent.mId, mId);
@@ -101,13 +92,4 @@ final class ChildContentCaptureSession extends ContentCaptureSession {
boolean isContentCaptureEnabled() {
return getMainCaptureSession().isContentCaptureEnabled();
}
@Override
void dump(String prefix, PrintWriter pw) {
if (mClientContext != null) {
// NOTE: we don't dump clientContent because it could have PII
pw.print(prefix); pw.println("hasClientContext");
}
super.dump(prefix, pw);
}
}

View File

@@ -91,13 +91,22 @@ public final class ContentCaptureEvent implements Parcelable {
*/
public static final int TYPE_INITIAL_VIEW_TREE_APPEARED = 5;
/**
* Called after a call to
* {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}.
*
* <p>The passed context is available through {@link #getContentCaptureContext()}.
*/
public static final int TYPE_CONTEXT_UPDATED = 6;
/** @hide */
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_VIEW_APPEARED,
TYPE_VIEW_DISAPPEARED,
TYPE_VIEW_TEXT_CHANGED,
TYPE_INITIAL_VIEW_TREE_APPEARING,
TYPE_INITIAL_VIEW_TREE_APPEARED
TYPE_INITIAL_VIEW_TREE_APPEARED,
TYPE_CONTEXT_UPDATED
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType{}
@@ -193,12 +202,13 @@ public final class ContentCaptureEvent implements Parcelable {
}
/**
* Used by {@link #TYPE_SESSION_STARTED}.
* Gets the {@link ContentCaptureContext} set calls to
* {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}.
*
* @hide
* <p>Only set on {@link #TYPE_CONTEXT_UPDATED} events.
*/
@Nullable
public ContentCaptureContext getClientContext() {
public ContentCaptureContext getContentCaptureContext() {
return mClientContext;
}
@@ -220,8 +230,8 @@ public final class ContentCaptureEvent implements Parcelable {
* Gets the type of the event.
*
* @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED},
* {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_INITIAL_VIEW_TREE_APPEARING}, or
* {@link #TYPE_INITIAL_VIEW_TREE_APPEARED}.
* {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_INITIAL_VIEW_TREE_APPEARING},
* {@link #TYPE_INITIAL_VIEW_TREE_APPEARED}, or {@link #TYPE_CONTEXT_UPDATED}.
*/
public @EventType int getType() {
return mType;
@@ -299,6 +309,10 @@ public final class ContentCaptureEvent implements Parcelable {
if (mText != null) {
pw.print(", text="); pw.println(getSanitizedString(mText));
}
if (mClientContext != null) {
pw.print(", context="); mClientContext.dump(pw); pw.println();
}
}
@Override
@@ -325,6 +339,9 @@ public final class ContentCaptureEvent implements Parcelable {
if (mText != null) {
string.append(", text=").append(getSanitizedString(mText));
}
if (mClientContext != null) {
string.append(", context=").append(mClientContext);
}
return string.append(']').toString();
}
@@ -345,7 +362,7 @@ public final class ContentCaptureEvent implements Parcelable {
if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) {
parcel.writeString(mParentSessionId);
}
if (mType == TYPE_SESSION_STARTED) {
if (mType == TYPE_SESSION_STARTED || mType == TYPE_CONTEXT_UPDATED) {
parcel.writeParcelable(mClientContext, flags);
}
}
@@ -375,7 +392,7 @@ public final class ContentCaptureEvent implements Parcelable {
if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) {
event.setParentSessionId(parcel.readString());
}
if (type == TYPE_SESSION_STARTED) {
if (type == TYPE_SESSION_STARTED || type == TYPE_CONTEXT_UPDATED) {
event.setClientContext(parcel.readParcelable(null));
}
return event;
@@ -404,6 +421,8 @@ public final class ContentCaptureEvent implements Parcelable {
return "INITIAL_VIEW_HIERARCHY_STARTED";
case TYPE_INITIAL_VIEW_TREE_APPEARED:
return "INITIAL_VIEW_HIERARCHY_FINISHED";
case TYPE_CONTEXT_UPDATED:
return "CONTEXT_UPDATED";
default:
return "UKNOWN_TYPE: " + type;
}

View File

@@ -165,6 +165,14 @@ public abstract class ContentCaptureSession implements AutoCloseable {
// 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 ContentCaptureContext mClientContext;
/**
* List of children session.
*/
@@ -183,6 +191,12 @@ public abstract class ContentCaptureSession implements AutoCloseable {
mId = Preconditions.checkNotNull(id);
}
// Used by ChildCOntentCaptureSession
ContentCaptureSession(@NonNull ContentCaptureContext initialContext) {
this();
mClientContext = Preconditions.checkNotNull(initialContext);
}
/** @hide */
@NonNull
abstract MainContentCaptureSession getMainCaptureSession();
@@ -239,6 +253,30 @@ public abstract class ContentCaptureSession implements AutoCloseable {
*/
abstract void flush(@FlushReason int reason);
/**
* Sets the {@link ContentCaptureContext} associated with the session.
*
* <p>Typically used to change the context associated with the default session from an activity.
*/
public final void setContentCaptureContext(@Nullable ContentCaptureContext context) {
mClientContext = context;
updateContentCaptureContext(context);
}
abstract void updateContentCaptureContext(@Nullable ContentCaptureContext context);
/**
* Gets the {@link ContentCaptureContext} associated with the session.
*
* @return context set on constructor or by
* {@link #setContentCaptureContext(ContentCaptureContext)}, or {@code null} if never
* explicitly set.
*/
@Nullable
public final ContentCaptureContext getContentCaptureContext() {
return mClientContext;
}
/**
* Destroys this session, flushing out all pending notifications to the service.
*
@@ -424,6 +462,9 @@ public abstract class ContentCaptureSession implements AutoCloseable {
@CallSuper
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("id: "); pw.println(mId);
if (mClientContext != null) {
pw.print(prefix); mClientContext.dump(pw); pw.println();
}
synchronized (mLock) {
pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
if (mChildren != null && !mChildren.isEmpty()) {

View File

@@ -15,6 +15,7 @@
*/
package android.view.contentcapture;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_INITIAL_VIEW_TREE_APPEARING;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
@@ -269,11 +270,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
final int eventType = event.getType();
if (VERBOSE) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED) {
if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
&& eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
// TODO(b/120494182): comment when this could happen (dialogs?)
Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
+ ContentCaptureEvent.getTypeAsString(eventType)
+ "): session not started yet");
+ "): dropping because session not started yet");
return;
}
if (mDisabled.get()) {
@@ -476,6 +478,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
}
}
@Override
public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
notifyContextUpdated(mId, context);
}
/**
* Resets the buffer and return a {@link ParceledListSlice} with the previous events.
*/
@@ -613,6 +620,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
}
}
void notifyContextUpdated(@NonNull String sessionId,
@Nullable ContentCaptureContext context) {
sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
.setClientContext(context));
}
@Override
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
pw.print(prefix); pw.print("mContext: "); pw.println(mContext);

View File

@@ -15,6 +15,7 @@
*/
package android.view.contentcapture;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
@@ -174,7 +175,8 @@ public class ContentCaptureEventTest {
assertThat(event.getIds()).isNull();
assertThat(event.getText()).isNull();
assertThat(event.getViewNode()).isNull();
final ContentCaptureContext clientContext = event.getClientContext();
final ContentCaptureContext clientContext = event.getContentCaptureContext();
assertThat(clientContext).isNotNull();
assertThat(clientContext.getAction()).isEqualTo("WHATEVER");
}
@@ -205,9 +207,44 @@ public class ContentCaptureEventTest {
assertThat(event.getIds()).isNull();
assertThat(event.getText()).isNull();
assertThat(event.getViewNode()).isNull();
assertThat(event.getClientContext()).isNull();
assertThat(event.getContentCaptureContext()).isNull();
}
@Test
public void testContextUpdated_directly() {
final ContentCaptureEvent event = new ContentCaptureEvent("42", TYPE_CONTEXT_UPDATED)
.setClientContext(mClientContext);
assertThat(event).isNotNull();
assertContextUpdatedEvent(event);
}
@Test
public void testContextUpdated_throughParcel() {
final ContentCaptureEvent event = new ContentCaptureEvent("42", TYPE_CONTEXT_UPDATED)
.setClientContext(mClientContext);
assertThat(event).isNotNull();
final ContentCaptureEvent clone = cloneThroughParcel(event);
assertContextUpdatedEvent(clone);
}
private void assertContextUpdatedEvent(ContentCaptureEvent event) {
assertThat(event.getType()).isEqualTo(TYPE_CONTEXT_UPDATED);
assertThat(event.getEventTime()).isAtLeast(MY_EPOCH);
assertThat(event.getSessionId()).isEqualTo("42");
assertThat(event.getParentSessionId()).isNull();
assertThat(event.getId()).isNull();
assertThat(event.getIds()).isNull();
assertThat(event.getText()).isNull();
assertThat(event.getViewNode()).isNull();
final ContentCaptureContext clientContext = event.getContentCaptureContext();
assertThat(clientContext).isNotNull();
assertThat(clientContext.getAction()).isEqualTo("WHATEVER");
}
// TODO(b/123036895): add test for all events type (right now we're just testing the 3 types
// that use logic to write to parcel
private ContentCaptureEvent cloneThroughParcel(ContentCaptureEvent event) {
Parcel parcel = Parcel.obtain();

View File

@@ -160,5 +160,10 @@ public class ContentCaptureSessionTest {
public void internalNotifyViewHierarchyEvent(boolean started) {
throw new UnsupportedOperationException("should not have been called");
}
@Override
public void updateContentCaptureContext(ContentCaptureContext context) {
throw new UnsupportedOperationException("should not have been called");
}
}
}

View File

@@ -37,6 +37,9 @@ final class ContentCaptureServerSession {
final IBinder mActivityToken;
private final ContentCapturePerUserService mService;
private final RemoteContentCaptureService mRemoteService;
// NOTE: this is the "internal" context (like package and taskId), not the explicit content
// set by apps - those are only send to the ContentCaptureService.
private final ContentCaptureContext mContentCaptureContext;
/**