Merge "Update the content capture text changed event merge logic" into sc-dev

This commit is contained in:
TYM Tsai
2021-03-25 09:19:16 +00:00
committed by Android (Google) Code Review
3 changed files with 85 additions and 25 deletions

View File

@@ -143,6 +143,9 @@ public final class ContentCaptureEvent implements Parcelable {
private @Nullable ContentCaptureContext mClientContext; private @Nullable ContentCaptureContext mClientContext;
private @Nullable Insets mInsets; private @Nullable Insets mInsets;
/** Only used in the main Content Capture session, no need to parcel */
private boolean mTextHasComposingSpan;
/** @hide */ /** @hide */
public ContentCaptureEvent(int sessionId, int type, long eventTime) { public ContentCaptureEvent(int sessionId, int type, long eventTime) {
mSessionId = sessionId; mSessionId = sessionId;
@@ -243,11 +246,21 @@ public final class ContentCaptureEvent implements Parcelable {
/** @hide */ /** @hide */
@NonNull @NonNull
public ContentCaptureEvent setText(@Nullable CharSequence text) { public ContentCaptureEvent setText(@Nullable CharSequence text, boolean hasComposingSpan) {
mText = text; mText = text;
mTextHasComposingSpan = hasComposingSpan;
return this; return this;
} }
/**
* The value is not parcelled, become false after parcelled.
* @hide
*/
@NonNull
public boolean getTextHasComposingSpan() {
return mTextHasComposingSpan;
}
/** @hide */ /** @hide */
@NonNull @NonNull
public ContentCaptureEvent setInsets(@NonNull Insets insets) { public ContentCaptureEvent setInsets(@NonNull Insets insets) {
@@ -361,7 +374,7 @@ public final class ContentCaptureEvent implements Parcelable {
throw new IllegalArgumentException("mergeEvent(): got " throw new IllegalArgumentException("mergeEvent(): got "
+ "TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event); + "TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event);
} else if (eventType == TYPE_VIEW_TEXT_CHANGED) { } else if (eventType == TYPE_VIEW_TEXT_CHANGED) {
setText(event.getText()); setText(event.getText(), event.getTextHasComposingSpan());
} else { } else {
Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType) Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType)
+ ") does not support this event type."); + ") does not support this event type.");
@@ -479,7 +492,7 @@ public final class ContentCaptureEvent implements Parcelable {
if (node != null) { if (node != null) {
event.setViewNode(node); event.setViewNode(node);
} }
event.setText(parcel.readCharSequence()); event.setText(parcel.readCharSequence(), false);
if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) { if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) {
event.setParentSessionId(parcel.readInt()); event.setParentSessionId(parcel.readInt());
} }

View File

@@ -43,12 +43,15 @@ import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.IBinder.DeathRecipient; import android.os.IBinder.DeathRecipient;
import android.os.RemoteException; import android.os.RemoteException;
import android.text.Spannable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.LocalLog; import android.util.LocalLog;
import android.util.Log; import android.util.Log;
import android.util.TimeUtils; import android.util.TimeUtils;
import android.view.autofill.AutofillId; import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl; import android.view.contentcapture.ViewNode.ViewStructureImpl;
import android.view.inputmethod.BaseInputConnection;
import com.android.internal.os.IResultReceiver; import com.android.internal.os.IResultReceiver;
@@ -57,6 +60,7 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
@@ -146,6 +150,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
@Nullable @Nullable
private final LocalLog mFlushHistory; private final LocalLog mFlushHistory;
/**
* If the event in the buffer is of type {@link TYPE_VIEW_TEXT_CHANGED}, this value
* indicates whether the event has composing span or not.
*/
private final Map<AutofillId, Boolean> mLastComposingSpan = new ArrayMap<>();
/** /**
* Binder object used to update the session state. * Binder object used to update the session state.
*/ */
@@ -335,26 +345,47 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
// Some type of events can be merged together // Some type of events can be merged together
boolean addEvent = true; boolean addEvent = true;
if (!mEvents.isEmpty() && eventType == TYPE_VIEW_TEXT_CHANGED) { if (eventType == TYPE_VIEW_TEXT_CHANGED) {
final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1); // We determine whether to add or merge the current event by following criteria:
// 1. Don't have composing span: always add.
// 2. Have composing span:
// 2.1 either last or current text is empty: add.
// 2.2 last event doesn't have composing span: add.
// Otherwise, merge.
// We merge two consecutive text change event, unless one of them clears the text. final CharSequence text = event.getText();
if (lastEvent.getType() == TYPE_VIEW_TEXT_CHANGED final boolean textHasComposingSpan = event.getTextHasComposingSpan();
&& lastEvent.getId().equals(event.getId())) {
boolean bothNonEmpty = !TextUtils.isEmpty(lastEvent.getText()) if (textHasComposingSpan && !mLastComposingSpan.isEmpty()) {
&& !TextUtils.isEmpty(event.getText()); final Boolean lastEventHasComposingSpan = mLastComposingSpan.get(event.getId());
boolean equalContent = TextUtils.equals(lastEvent.getText(), event.getText()); if (lastEventHasComposingSpan != null && lastEventHasComposingSpan.booleanValue()) {
if (equalContent) { ContentCaptureEvent lastEvent = null;
addEvent = false; for (int index = mEvents.size() - 1; index >= 0; index--) {
} else if (bothNonEmpty) { final ContentCaptureEvent tmpEvent = mEvents.get(index);
lastEvent.mergeEvent(event); if (event.getId().equals(tmpEvent.getId())) {
addEvent = false; lastEvent = tmpEvent;
} break;
if (!addEvent && sVerbose) { }
Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text=" }
+ getSanitizedString(event.getText())); if (lastEvent != null) {
final CharSequence lastText = lastEvent.getText();
final boolean bothNonEmpty = !TextUtils.isEmpty(lastText)
&& !TextUtils.isEmpty(text);
boolean equalContent = TextUtils.equals(lastText, text);
if (equalContent) {
addEvent = false;
} else if (bothNonEmpty && lastEventHasComposingSpan) {
lastEvent.mergeEvent(event);
addEvent = false;
}
if (!addEvent && sVerbose) {
Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
+ getSanitizedString(text));
}
}
} }
} }
mLastComposingSpan.put(event.getId(), textHasComposingSpan);
} }
if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) { if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
@@ -374,6 +405,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
mEvents.add(event); mEvents.add(event);
} }
// TODO: we need to change when the flush happens so that we don't flush while the
// composing span hasn't changed. But we might need to keep flushing the events for the
// non-editable views and views that don't have the composing state; otherwise some other
// Content Capture features may be delayed.
final int numberEvents = mEvents.size(); final int numberEvents = mEvents.size();
final boolean bufferEvent = numberEvents < maxBufferSize; final boolean bufferEvent = numberEvents < maxBufferSize;
@@ -550,6 +586,7 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
? Collections.emptyList() ? Collections.emptyList()
: mEvents; : mEvents;
mEvents = null; mEvents = null;
mLastComposingSpan.clear();
return new ParceledListSlice<>(events); return new ParceledListSlice<>(events);
} }
@@ -677,9 +714,16 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
} }
void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) { void notifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
// Since the same CharSequence instance may be reused in the TextView, we need to make
// a copy of its content so that its value will not be changed by subsequent updates
// in the TextView.
final String eventText = text == null ? null : text.toString();
final boolean textHasComposingSpan =
text instanceof Spannable && BaseInputConnection.getComposingSpanStart(
(Spannable) text) >= 0;
mHandler.post(() -> sendEvent( mHandler.post(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED) new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
.setAutofillId(id).setText(text))); .setAutofillId(id).setText(eventText, textHasComposingSpan)));
} }
/** Public because is also used by ViewRootImpl */ /** Public because is also used by ViewRootImpl */

View File

@@ -236,12 +236,13 @@ public class ContentCaptureEventTest {
@Test @Test
public void testMergeEvent_typeViewTextChanged() { public void testMergeEvent_typeViewTextChanged() {
final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_TEXT_CHANGED) final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_TEXT_CHANGED)
.setText("test"); .setText("test", false);
final ContentCaptureEvent event2 = new ContentCaptureEvent(43, TYPE_VIEW_TEXT_CHANGED) final ContentCaptureEvent event2 = new ContentCaptureEvent(43, TYPE_VIEW_TEXT_CHANGED)
.setText("empty"); .setText("empty", true);
event.mergeEvent(event2); event.mergeEvent(event2);
assertThat(event.getText()).isEqualTo(event2.getText()); assertThat(event.getText()).isEqualTo(event2.getText());
assertThat(event.getTextHasComposingSpan()).isEqualTo(event2.getTextHasComposingSpan());
} }
@Test @Test
@@ -282,16 +283,18 @@ public class ContentCaptureEventTest {
@Test @Test
public void testMergeEvent_differentEventTypes() { public void testMergeEvent_differentEventTypes() {
final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_DISAPPEARED) final ContentCaptureEvent event = new ContentCaptureEvent(42, TYPE_VIEW_DISAPPEARED)
.setText("test").setAutofillId(new AutofillId(1)); .setText("test", false).setAutofillId(new AutofillId(1));
final ContentCaptureEvent event2 = new ContentCaptureEvent(17, TYPE_VIEW_TEXT_CHANGED) final ContentCaptureEvent event2 = new ContentCaptureEvent(17, TYPE_VIEW_TEXT_CHANGED)
.setText("empty").setAutofillId(new AutofillId(2)); .setText("empty", true).setAutofillId(new AutofillId(2));
event.mergeEvent(event2); event.mergeEvent(event2);
assertThat(event.getText()).isEqualTo("test"); assertThat(event.getText()).isEqualTo("test");
assertThat(event.getTextHasComposingSpan()).isFalse();
assertThat(event.getId()).isEqualTo(new AutofillId(1)); assertThat(event.getId()).isEqualTo(new AutofillId(1));
event2.mergeEvent(event); event2.mergeEvent(event);
assertThat(event2.getText()).isEqualTo("empty"); assertThat(event2.getText()).isEqualTo("empty");
assertThat(event2.getTextHasComposingSpan()).isTrue();
assertThat(event2.getId()).isEqualTo(new AutofillId(2)); assertThat(event2.getId()).isEqualTo(new AutofillId(2));
} }