blob: ce6d034c585eca49681f908143cc3e51608d7406 [file] [log] [blame]
/*
* 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.ContentCaptureHelper.getSanitizedString;
import static android.view.contentcapture.ContentCaptureManager.DEBUG;
import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.graphics.Insets;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.inputmethod.BaseInputConnection;
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/** @hide */
@SystemApi
public final class ContentCaptureEvent implements Parcelable {
private static final String TAG = ContentCaptureEvent.class.getSimpleName();
/** @hide */
public static final int TYPE_SESSION_FINISHED = -2;
/** @hide */
public static final int TYPE_SESSION_STARTED = -1;
/**
* 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 = 1;
/**
* Called when one or more nodes have been removed from the screen and is not visible to the
* user anymore.
*
* <p>To get the id(s), first call {@link #getIds()} - if it returns {@code null}, then call
* {@link #getId()}.
*/
public static final int TYPE_VIEW_DISAPPEARED = 2;
/**
* Called when the text of a node has been changed.
*
* <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 = 3;
/**
* Called before events (such as {@link #TYPE_VIEW_APPEARED} and/or
* {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy are sent.
*
* <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
* if the initial view hierarchy doesn't initially have any view that's important for content
* capture.
*/
public static final int TYPE_VIEW_TREE_APPEARING = 4;
/**
* Called after events (such as {@link #TYPE_VIEW_APPEARED} and/or
* {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy were sent.
*
* <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent
* if the initial view hierarchy doesn't initially have any view that's important for content
* capture.
*/
public static final int TYPE_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;
/**
* Called after the session is ready, typically after the activity resumed and the
* initial views appeared
*/
public static final int TYPE_SESSION_RESUMED = 7;
/**
* Called after the session is paused, typically after the activity paused and the
* views disappeared.
*/
public static final int TYPE_SESSION_PAUSED = 8;
/**
* Called when the view's insets are changed. The new insets associated with the
* event may then be retrieved by calling {@link #getInsets()}
*/
public static final int TYPE_VIEW_INSETS_CHANGED = 9;
/** @hide */
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_VIEW_APPEARED,
TYPE_VIEW_DISAPPEARED,
TYPE_VIEW_TEXT_CHANGED,
TYPE_VIEW_TREE_APPEARING,
TYPE_VIEW_TREE_APPEARED,
TYPE_CONTEXT_UPDATED,
TYPE_SESSION_PAUSED,
TYPE_SESSION_RESUMED,
TYPE_VIEW_INSETS_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType{}
/** @hide */
public static final int MAX_INVALID_VALUE = -1;
private final int mSessionId;
private final int mType;
private final long mEventTime;
private @Nullable AutofillId mId;
private @Nullable ArrayList<AutofillId> mIds;
private @Nullable ViewNode mNode;
private @Nullable CharSequence mText;
private int mParentSessionId = NO_SESSION_ID;
private @Nullable ContentCaptureContext mClientContext;
private @Nullable Insets mInsets;
private int mComposingStart = MAX_INVALID_VALUE;
private int mComposingEnd = MAX_INVALID_VALUE;
private int mSelectionStartIndex = MAX_INVALID_VALUE;
private int mSelectionEndIndex = MAX_INVALID_VALUE;
/** Only used in the main Content Capture session, no need to parcel */
private boolean mTextHasComposingSpan;
/** @hide */
public ContentCaptureEvent(int sessionId, int type, long eventTime) {
mSessionId = sessionId;
mType = type;
mEventTime = eventTime;
}
/** @hide */
public ContentCaptureEvent(int sessionId, int type) {
this(sessionId, type, System.currentTimeMillis());
}
/** @hide */
public ContentCaptureEvent setAutofillId(@NonNull AutofillId id) {
mId = Preconditions.checkNotNull(id);
return this;
}
/** @hide */
public ContentCaptureEvent setAutofillIds(@NonNull ArrayList<AutofillId> ids) {
mIds = Preconditions.checkNotNull(ids);
return this;
}
/**
* Adds an autofill id to the this event, merging the single id into a list if necessary.
*
* @hide
*/
public ContentCaptureEvent addAutofillId(@NonNull AutofillId id) {
Preconditions.checkNotNull(id);
if (mIds == null) {
mIds = new ArrayList<>();
if (mId == null) {
Log.w(TAG, "addAutofillId(" + id + ") called without an initial id");
} else {
mIds.add(mId);
mId = null;
}
}
mIds.add(id);
return this;
}
/**
* Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
*
* @hide
*/
public ContentCaptureEvent setParentSessionId(int parentSessionId) {
mParentSessionId = parentSessionId;
return this;
}
/**
* Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
*
* @hide
*/
public ContentCaptureEvent setClientContext(@NonNull ContentCaptureContext clientContext) {
mClientContext = clientContext;
return this;
}
/** @hide */
@NonNull
public int getSessionId() {
return mSessionId;
}
/**
* Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}.
*
* @hide
*/
@Nullable
public int getParentSessionId() {
return mParentSessionId;
}
/**
* Gets the {@link ContentCaptureContext} set calls to
* {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}.
*
* <p>Only set on {@link #TYPE_CONTEXT_UPDATED} events.
*/
@Nullable
public ContentCaptureContext getContentCaptureContext() {
return mClientContext;
}
/** @hide */
@NonNull
public ContentCaptureEvent setViewNode(@NonNull ViewNode node) {
mNode = Preconditions.checkNotNull(node);
return this;
}
/** @hide */
@NonNull
public ContentCaptureEvent setText(@Nullable CharSequence text) {
mText = text;
return this;
}
/** @hide */
@NonNull
public ContentCaptureEvent setComposingIndex(int start, int end) {
mComposingStart = start;
mComposingEnd = end;
return this;
}
/** @hide */
@NonNull
public boolean hasComposingSpan() {
return mComposingStart > MAX_INVALID_VALUE;
}
/** @hide */
@NonNull
public ContentCaptureEvent setSelectionIndex(int start, int end) {
mSelectionStartIndex = start;
mSelectionEndIndex = end;
return this;
}
boolean hasSameComposingSpan(@NonNull ContentCaptureEvent other) {
return mComposingStart == other.mComposingStart && mComposingEnd == other.mComposingEnd;
}
boolean hasSameSelectionSpan(@NonNull ContentCaptureEvent other) {
return mSelectionStartIndex == other.mSelectionStartIndex
&& mSelectionEndIndex == other.mSelectionEndIndex;
}
private int getComposingStart() {
return mComposingStart;
}
private int getComposingEnd() {
return mComposingEnd;
}
private int getSelectionStart() {
return mSelectionStartIndex;
}
private int getSelectionEnd() {
return mSelectionEndIndex;
}
private void restoreComposingSpan() {
if (mComposingStart <= MAX_INVALID_VALUE
|| mComposingEnd <= MAX_INVALID_VALUE) {
return;
}
if (mText instanceof Spannable) {
BaseInputConnection.setComposingSpans((Spannable) mText, mComposingStart,
mComposingEnd);
} else {
Log.w(TAG, "Text is not a Spannable.");
}
}
private void restoreSelectionSpans() {
if (mSelectionStartIndex <= MAX_INVALID_VALUE
|| mSelectionEndIndex <= MAX_INVALID_VALUE) {
return;
}
if (mText instanceof SpannableString) {
SpannableString ss = (SpannableString) mText;
ss.setSpan(Selection.SELECTION_START, mSelectionStartIndex, mSelectionStartIndex, 0);
ss.setSpan(Selection.SELECTION_END, mSelectionEndIndex, mSelectionEndIndex, 0);
} else {
Log.w(TAG, "Text is not a SpannableString.");
}
}
/** @hide */
@NonNull
public ContentCaptureEvent setInsets(@NonNull Insets insets) {
mInsets = insets;
return this;
}
/**
* Gets the type of the event.
*
* @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED},
* {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_VIEW_TREE_APPEARING},
* {@link #TYPE_VIEW_TREE_APPEARED}, {@link #TYPE_CONTEXT_UPDATED},
* {@link #TYPE_SESSION_RESUMED}, or {@link #TYPE_SESSION_PAUSED}.
*/
public @EventType int getType() {
return mType;
}
/**
* Gets when the event was generated, in millis since epoch.
*/
public long getEventTime() {
return mEventTime;
}
/**
* Gets the whole metadata of the node associated with the event.
*
* <p>Only set on {@link #TYPE_VIEW_APPEARED} events.
*/
@Nullable
public ViewNode getViewNode() {
return mNode;
}
/**
* Gets the {@link AutofillId} of the node associated with the event.
*
* <p>Only set on {@link #TYPE_VIEW_DISAPPEARED} (when the event contains just one node - if
* it contains more than one, this method returns {@code null} and the actual ids should be
* retrived by {@link #getIds()}) and {@link #TYPE_VIEW_TEXT_CHANGED} events.
*/
@Nullable
public AutofillId getId() {
return mId;
}
/**
* Gets the {@link AutofillId AutofillIds} of the nodes associated with the event.
*
* <p>Only set on {@link #TYPE_VIEW_DISAPPEARED}, when the event contains more than one node
* (if it contains just one node, it's returned by {@link #getId()} instead.
*/
@Nullable
public List<AutofillId> getIds() {
return mIds;
}
/**
* Gets the current text of the node associated with the event.
*
* <p>Only set on {@link #TYPE_VIEW_TEXT_CHANGED} events.
*/
@Nullable
public CharSequence getText() {
return mText;
}
/**
* Gets the rectangle of the insets associated with the event. Valid insets will only be
* returned if the type of the event is {@link #TYPE_VIEW_INSETS_CHANGED}, otherwise they
* will be null.
*/
@Nullable
public Insets getInsets() {
return mInsets;
}
/**
* Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
* or {@link #TYPE_VIEW_DISAPPEARED}.
*
* @hide
*/
public void mergeEvent(@NonNull ContentCaptureEvent event) {
Preconditions.checkNotNull(event);
final int eventType = event.getType();
if (mType != eventType) {
Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType) + ") cannot be merged "
+ "with different eventType=" + getTypeAsString(mType));
return;
}
if (eventType == TYPE_VIEW_DISAPPEARED) {
final List<AutofillId> ids = event.getIds();
final AutofillId id = event.getId();
if (ids != null) {
if (id != null) {
Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event);
}
for (int i = 0; i < ids.size(); i++) {
addAutofillId(ids.get(i));
}
return;
}
if (id != null) {
addAutofillId(id);
return;
}
throw new IllegalArgumentException("mergeEvent(): got "
+ "TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
} else if (eventType == TYPE_VIEW_TEXT_CHANGED) {
setText(event.getText());
setComposingIndex(event.getComposingStart(), event.getComposingEnd());
setSelectionIndex(event.getSelectionStart(), event.getSelectionEnd());
} else {
Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType)
+ ") does not support this event type.");
}
}
/** @hide */
public void dump(@NonNull PrintWriter pw) {
pw.print("type="); pw.print(getTypeAsString(mType));
pw.print(", time="); pw.print(mEventTime);
if (mId != null) {
pw.print(", id="); pw.print(mId);
}
if (mIds != null) {
pw.print(", ids="); pw.print(mIds);
}
if (mNode != null) {
pw.print(", mNode.id="); pw.print(mNode.getAutofillId());
}
if (mSessionId != NO_SESSION_ID) {
pw.print(", sessionId="); pw.print(mSessionId);
}
if (mParentSessionId != NO_SESSION_ID) {
pw.print(", parentSessionId="); pw.print(mParentSessionId);
}
if (mText != null) {
pw.print(", text="); pw.println(getSanitizedString(mText));
}
if (mClientContext != null) {
pw.print(", context="); mClientContext.dump(pw); pw.println();
}
if (mInsets != null) {
pw.print(", insets="); pw.println(mInsets);
}
if (mComposingStart > MAX_INVALID_VALUE) {
pw.print(", composing("); pw.print(mComposingStart);
pw.print(", "); pw.print(mComposingEnd); pw.print(")");
}
if (mSelectionStartIndex > MAX_INVALID_VALUE) {
pw.print(", selection("); pw.print(mSelectionStartIndex);
pw.print(", "); pw.print(mSelectionEndIndex); pw.print(")");
}
}
@NonNull
@Override
public String toString() {
final StringBuilder string = new StringBuilder("ContentCaptureEvent[type=")
.append(getTypeAsString(mType));
string.append(", session=").append(mSessionId);
if (mType == TYPE_SESSION_STARTED && mParentSessionId != NO_SESSION_ID) {
string.append(", parent=").append(mParentSessionId);
}
if (mId != null) {
string.append(", id=").append(mId);
}
if (mIds != null) {
string.append(", ids=").append(mIds);
}
if (mNode != null) {
final String className = mNode.getClassName();
string.append(", class=").append(className);
string.append(", id=").append(mNode.getAutofillId());
if (mNode.getText() != null) {
string.append(", text=")
.append(DEBUG ? mNode.getText() : getSanitizedString(mNode.getText()));
}
}
if (mText != null) {
string.append(", text=")
.append(DEBUG ? mText : getSanitizedString(mText));
}
if (mClientContext != null) {
string.append(", context=").append(mClientContext);
}
if (mInsets != null) {
string.append(", insets=").append(mInsets);
}
if (mComposingStart > MAX_INVALID_VALUE) {
string.append(", composing=[")
.append(mComposingStart).append(",").append(mComposingEnd).append("]");
}
if (mSelectionStartIndex > MAX_INVALID_VALUE) {
string.append(", selection=[")
.append(mSelectionStartIndex).append(",")
.append(mSelectionEndIndex).append("]");
}
return string.append(']').toString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(mSessionId);
parcel.writeInt(mType);
parcel.writeLong(mEventTime);
parcel.writeParcelable(mId, flags);
parcel.writeTypedList(mIds);
ViewNode.writeToParcel(parcel, mNode, flags);
parcel.writeCharSequence(mText);
if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) {
parcel.writeInt(mParentSessionId);
}
if (mType == TYPE_SESSION_STARTED || mType == TYPE_CONTEXT_UPDATED) {
parcel.writeParcelable(mClientContext, flags);
}
if (mType == TYPE_VIEW_INSETS_CHANGED) {
parcel.writeParcelable(mInsets, flags);
}
if (mType == TYPE_VIEW_TEXT_CHANGED) {
parcel.writeInt(mComposingStart);
parcel.writeInt(mComposingEnd);
parcel.writeInt(mSelectionStartIndex);
parcel.writeInt(mSelectionEndIndex);
}
}
public static final @android.annotation.NonNull Parcelable.Creator<ContentCaptureEvent> CREATOR =
new Parcelable.Creator<ContentCaptureEvent>() {
@Override
@NonNull
public ContentCaptureEvent createFromParcel(Parcel parcel) {
final int sessionId = parcel.readInt();
final int type = parcel.readInt();
final long eventTime = parcel.readLong();
final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type, eventTime);
final AutofillId id = parcel.readParcelable(null);
if (id != null) {
event.setAutofillId(id);
}
final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR);
if (ids != null) {
event.setAutofillIds(ids);
}
final ViewNode node = ViewNode.readFromParcel(parcel);
if (node != null) {
event.setViewNode(node);
}
event.setText(parcel.readCharSequence());
if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) {
event.setParentSessionId(parcel.readInt());
}
if (type == TYPE_SESSION_STARTED || type == TYPE_CONTEXT_UPDATED) {
event.setClientContext(parcel.readParcelable(null));
}
if (type == TYPE_VIEW_INSETS_CHANGED) {
event.setInsets(parcel.readParcelable(null));
}
if (type == TYPE_VIEW_TEXT_CHANGED) {
event.setComposingIndex(parcel.readInt(), parcel.readInt());
event.restoreComposingSpan();
event.setSelectionIndex(parcel.readInt(), parcel.readInt());
event.restoreSelectionSpans();
}
return event;
}
@Override
@NonNull
public ContentCaptureEvent[] newArray(int size) {
return new ContentCaptureEvent[size];
}
};
/** @hide */
public static String getTypeAsString(@EventType int type) {
switch (type) {
case TYPE_SESSION_STARTED:
return "SESSION_STARTED";
case TYPE_SESSION_FINISHED:
return "SESSION_FINISHED";
case TYPE_SESSION_RESUMED:
return "SESSION_RESUMED";
case TYPE_SESSION_PAUSED:
return "SESSION_PAUSED";
case TYPE_VIEW_APPEARED:
return "VIEW_APPEARED";
case TYPE_VIEW_DISAPPEARED:
return "VIEW_DISAPPEARED";
case TYPE_VIEW_TEXT_CHANGED:
return "VIEW_TEXT_CHANGED";
case TYPE_VIEW_TREE_APPEARING:
return "VIEW_TREE_APPEARING";
case TYPE_VIEW_TREE_APPEARED:
return "VIEW_TREE_APPEARED";
case TYPE_CONTEXT_UPDATED:
return "CONTEXT_UPDATED";
case TYPE_VIEW_INSETS_CHANGED:
return "VIEW_INSETS_CHANGED";
default:
return "UKNOWN_TYPE: " + type;
}
}
}