Loading core/java/android/view/contentcapture/ContentCaptureEvent.java +106 −10 Original line number Diff line number Diff line Loading @@ -25,8 +25,12 @@ 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; Loading Loading @@ -132,6 +136,9 @@ public final class ContentCaptureEvent implements Parcelable { @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; Loading @@ -143,6 +150,11 @@ public final class ContentCaptureEvent implements Parcelable { 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; Loading Loading @@ -246,19 +258,75 @@ public final class ContentCaptureEvent implements Parcelable { /** @hide */ @NonNull public ContentCaptureEvent setText(@Nullable CharSequence text, boolean hasComposingSpan) { public ContentCaptureEvent setText(@Nullable CharSequence text) { mText = text; mTextHasComposingSpan = hasComposingSpan; return this; } /** * The value is not parcelled, become false after parcelled. * @hide */ /** @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 boolean getTextHasComposingSpan() { return mTextHasComposingSpan; public ContentCaptureEvent setSelectionIndex(int start, int end) { mSelectionStartIndex = start; mSelectionEndIndex = end; return this; } 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 */ Loading Loading @@ -374,7 +442,9 @@ public final class ContentCaptureEvent implements Parcelable { 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(), event.getTextHasComposingSpan()); 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."); Loading Loading @@ -409,6 +479,14 @@ public final class ContentCaptureEvent implements Parcelable { 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 Loading Loading @@ -443,6 +521,12 @@ public final class ContentCaptureEvent implements Parcelable { if (mInsets != null) { string.append(", insets=").append(mInsets); } if (mComposingStart > MAX_INVALID_VALUE) { string.append(", hasComposing"); } if (mSelectionStartIndex > MAX_INVALID_VALUE) { string.append(", hasSelection"); } return string.append(']').toString(); } Loading @@ -469,6 +553,12 @@ public final class ContentCaptureEvent implements Parcelable { 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 = Loading @@ -493,7 +583,7 @@ public final class ContentCaptureEvent implements Parcelable { if (node != null) { event.setViewNode(node); } event.setText(parcel.readCharSequence(), false); event.setText(parcel.readCharSequence()); if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) { event.setParentSessionId(parcel.readInt()); } Loading @@ -503,6 +593,12 @@ public final class ContentCaptureEvent implements Parcelable { 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; } Loading core/java/android/view/contentcapture/MainContentCaptureSession.java +20 −7 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.RemoteException; import android.text.Selection; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; Loading Loading @@ -347,8 +348,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // 2.2 last event doesn't have composing span: add. // Otherwise, merge. final CharSequence text = event.getText(); final boolean textHasComposingSpan = event.getTextHasComposingSpan(); if (textHasComposingSpan) { final boolean hasComposingSpan = event.hasComposingSpan(); if (hasComposingSpan) { ContentCaptureEvent lastEvent = null; for (int index = mEvents.size() - 1; index >= 0; index--) { final ContentCaptureEvent tmpEvent = mEvents.get(index); Loading @@ -357,7 +358,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { break; } } if (lastEvent != null && lastEvent.getTextHasComposingSpan()) { if (lastEvent != null && lastEvent.hasComposingSpan()) { final CharSequence lastText = lastEvent.getText(); final boolean bothNonEmpty = !TextUtils.isEmpty(lastText) && !TextUtils.isEmpty(text); Loading Loading @@ -705,12 +706,24 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // a copy of its content so that its value will not be changed by subsequent updates // in the TextView. final CharSequence eventText = stringOrSpannedStringWithoutNoCopySpans(text); final boolean textHasComposingSpan = text instanceof Spannable && BaseInputConnection.getComposingSpanStart( (Spannable) text) >= 0; final int composingStart; final int composingEnd; if (text instanceof Spannable) { composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text); composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text); } else { composingStart = ContentCaptureEvent.MAX_INVALID_VALUE; composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE; } final int startIndex = Selection.getSelectionStart(text); final int endIndex = Selection.getSelectionEnd(text); mHandler.post(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) .setAutofillId(id).setText(eventText, textHasComposingSpan))); .setAutofillId(id).setText(eventText) .setComposingIndex(composingStart, composingEnd) .setSelectionIndex(startIndex, endIndex))); } private CharSequence stringOrSpannedStringWithoutNoCopySpans(CharSequence source) { Loading core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java +8 −8 Original line number Diff line number Diff line Loading @@ -236,13 +236,13 @@ public class ContentCaptureEventTest { @Test public void testMergeEvent_typeViewTextChanged() { final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_TEXT_CHANGED) .setText("test", false); .setText("test"); final ContentCaptureEvent event2 = new ContentCaptureEvent(43, TYPE_VIEW_TEXT_CHANGED) .setText("empty", true); .setText("composing").setComposingIndex(0, 1); event.mergeEvent(event2); assertThat(event.getText()).isEqualTo(event2.getText()); assertThat(event.getTextHasComposingSpan()).isEqualTo(event2.getTextHasComposingSpan()); assertThat(event.hasComposingSpan()).isEqualTo(event2.hasComposingSpan()); } @Test Loading Loading @@ -283,18 +283,18 @@ public class ContentCaptureEventTest { @Test public void testMergeEvent_differentEventTypes() { final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_DISAPPEARED) .setText("test", false).setAutofillId(new AutofillId(1)); .setText("test").setAutofillId(new AutofillId(1)); final ContentCaptureEvent event2 = new ContentCaptureEvent(17, TYPE_VIEW_TEXT_CHANGED) .setText("empty", true).setAutofillId(new AutofillId(2)); .setText("composing").setAutofillId(new AutofillId(2)).setComposingIndex(0, 1); event.mergeEvent(event2); assertThat(event.getText()).isEqualTo("test"); assertThat(event.getTextHasComposingSpan()).isFalse(); assertThat(event.hasComposingSpan()).isFalse(); assertThat(event.getId()).isEqualTo(new AutofillId(1)); event2.mergeEvent(event); assertThat(event2.getText()).isEqualTo("empty"); assertThat(event2.getTextHasComposingSpan()).isTrue(); assertThat(event2.getText()).isEqualTo("composing"); assertThat(event2.hasComposingSpan()).isTrue(); assertThat(event2.getId()).isEqualTo(new AutofillId(2)); } Loading Loading
core/java/android/view/contentcapture/ContentCaptureEvent.java +106 −10 Original line number Diff line number Diff line Loading @@ -25,8 +25,12 @@ 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; Loading Loading @@ -132,6 +136,9 @@ public final class ContentCaptureEvent implements Parcelable { @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; Loading @@ -143,6 +150,11 @@ public final class ContentCaptureEvent implements Parcelable { 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; Loading Loading @@ -246,19 +258,75 @@ public final class ContentCaptureEvent implements Parcelable { /** @hide */ @NonNull public ContentCaptureEvent setText(@Nullable CharSequence text, boolean hasComposingSpan) { public ContentCaptureEvent setText(@Nullable CharSequence text) { mText = text; mTextHasComposingSpan = hasComposingSpan; return this; } /** * The value is not parcelled, become false after parcelled. * @hide */ /** @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 boolean getTextHasComposingSpan() { return mTextHasComposingSpan; public ContentCaptureEvent setSelectionIndex(int start, int end) { mSelectionStartIndex = start; mSelectionEndIndex = end; return this; } 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 */ Loading Loading @@ -374,7 +442,9 @@ public final class ContentCaptureEvent implements Parcelable { 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(), event.getTextHasComposingSpan()); 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."); Loading Loading @@ -409,6 +479,14 @@ public final class ContentCaptureEvent implements Parcelable { 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 Loading Loading @@ -443,6 +521,12 @@ public final class ContentCaptureEvent implements Parcelable { if (mInsets != null) { string.append(", insets=").append(mInsets); } if (mComposingStart > MAX_INVALID_VALUE) { string.append(", hasComposing"); } if (mSelectionStartIndex > MAX_INVALID_VALUE) { string.append(", hasSelection"); } return string.append(']').toString(); } Loading @@ -469,6 +553,12 @@ public final class ContentCaptureEvent implements Parcelable { 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 = Loading @@ -493,7 +583,7 @@ public final class ContentCaptureEvent implements Parcelable { if (node != null) { event.setViewNode(node); } event.setText(parcel.readCharSequence(), false); event.setText(parcel.readCharSequence()); if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) { event.setParentSessionId(parcel.readInt()); } Loading @@ -503,6 +593,12 @@ public final class ContentCaptureEvent implements Parcelable { 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; } Loading
core/java/android/view/contentcapture/MainContentCaptureSession.java +20 −7 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.RemoteException; import android.text.Selection; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; Loading Loading @@ -347,8 +348,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // 2.2 last event doesn't have composing span: add. // Otherwise, merge. final CharSequence text = event.getText(); final boolean textHasComposingSpan = event.getTextHasComposingSpan(); if (textHasComposingSpan) { final boolean hasComposingSpan = event.hasComposingSpan(); if (hasComposingSpan) { ContentCaptureEvent lastEvent = null; for (int index = mEvents.size() - 1; index >= 0; index--) { final ContentCaptureEvent tmpEvent = mEvents.get(index); Loading @@ -357,7 +358,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession { break; } } if (lastEvent != null && lastEvent.getTextHasComposingSpan()) { if (lastEvent != null && lastEvent.hasComposingSpan()) { final CharSequence lastText = lastEvent.getText(); final boolean bothNonEmpty = !TextUtils.isEmpty(lastText) && !TextUtils.isEmpty(text); Loading Loading @@ -705,12 +706,24 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // a copy of its content so that its value will not be changed by subsequent updates // in the TextView. final CharSequence eventText = stringOrSpannedStringWithoutNoCopySpans(text); final boolean textHasComposingSpan = text instanceof Spannable && BaseInputConnection.getComposingSpanStart( (Spannable) text) >= 0; final int composingStart; final int composingEnd; if (text instanceof Spannable) { composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text); composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text); } else { composingStart = ContentCaptureEvent.MAX_INVALID_VALUE; composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE; } final int startIndex = Selection.getSelectionStart(text); final int endIndex = Selection.getSelectionEnd(text); mHandler.post(() -> sendEvent( new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) .setAutofillId(id).setText(eventText, textHasComposingSpan))); .setAutofillId(id).setText(eventText) .setComposingIndex(composingStart, composingEnd) .setSelectionIndex(startIndex, endIndex))); } private CharSequence stringOrSpannedStringWithoutNoCopySpans(CharSequence source) { Loading
core/tests/coretests/src/android/view/contentcapture/ContentCaptureEventTest.java +8 −8 Original line number Diff line number Diff line Loading @@ -236,13 +236,13 @@ public class ContentCaptureEventTest { @Test public void testMergeEvent_typeViewTextChanged() { final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_TEXT_CHANGED) .setText("test", false); .setText("test"); final ContentCaptureEvent event2 = new ContentCaptureEvent(43, TYPE_VIEW_TEXT_CHANGED) .setText("empty", true); .setText("composing").setComposingIndex(0, 1); event.mergeEvent(event2); assertThat(event.getText()).isEqualTo(event2.getText()); assertThat(event.getTextHasComposingSpan()).isEqualTo(event2.getTextHasComposingSpan()); assertThat(event.hasComposingSpan()).isEqualTo(event2.hasComposingSpan()); } @Test Loading Loading @@ -283,18 +283,18 @@ public class ContentCaptureEventTest { @Test public void testMergeEvent_differentEventTypes() { final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_DISAPPEARED) .setText("test", false).setAutofillId(new AutofillId(1)); .setText("test").setAutofillId(new AutofillId(1)); final ContentCaptureEvent event2 = new ContentCaptureEvent(17, TYPE_VIEW_TEXT_CHANGED) .setText("empty", true).setAutofillId(new AutofillId(2)); .setText("composing").setAutofillId(new AutofillId(2)).setComposingIndex(0, 1); event.mergeEvent(event2); assertThat(event.getText()).isEqualTo("test"); assertThat(event.getTextHasComposingSpan()).isFalse(); assertThat(event.hasComposingSpan()).isFalse(); assertThat(event.getId()).isEqualTo(new AutofillId(1)); event2.mergeEvent(event); assertThat(event2.getText()).isEqualTo("empty"); assertThat(event2.getTextHasComposingSpan()).isTrue(); assertThat(event2.getText()).isEqualTo("composing"); assertThat(event2.hasComposingSpan()).isTrue(); assertThat(event2.getId()).isEqualTo(new AutofillId(2)); } Loading