/* * 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.graphics.Rect; 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 java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** @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. * * On API level 33, this event may be re-sent with additional information if a view's children * have changed, e.g. scrolling Views inside of a ListView. This information will be stored in * the extras Bundle associated with the event's ViewNode. Within the Bundle, the * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" key may be used to get a list of * Autofill IDs of active child views, and the * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" key may be used to get the 0-based * position of the first active child view in the list relative to the positions of child views * in the container View's dataset. * *
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. * *
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. * *
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. * *
NOTE: 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. * *
NOTE: 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)}. * *
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;
/**
* Called before {@link #TYPE_VIEW_TREE_APPEARING}, or after the size of the window containing
* the views changed.
*/
public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10;
/** @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,
TYPE_WINDOW_BOUNDS_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 Only set on {@link #TYPE_CONTEXT_UPDATED} events.
*/
@Nullable
public ContentCaptureContext getContentCaptureContext() {
return mClientContext;
}
/** @hide */
@NonNull
public ContentCaptureEvent setViewNode(@NonNull ViewNode node) {
mNode = Objects.requireNonNull(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;
}
/** @hide */
@NonNull
public ContentCaptureEvent setBounds(@NonNull Rect bounds) {
mBounds = bounds;
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.
*
* 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.
*
* 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.
*
* 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 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;
}
/**
* Gets the {@link Rect} bounds of the window associated with the event. Valid bounds will only
* be returned if the type of the event is {@link #TYPE_WINDOW_BOUNDS_CHANGED}, otherwise they
* will be null.
*/
@Nullable
public Rect getBounds() {
return mBounds;
}
/**
* Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
* or {@link #TYPE_VIEW_DISAPPEARED}.
*
* @hide
*/
public void mergeEvent(@NonNull ContentCaptureEvent event) {
Objects.requireNonNull(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