Merge "Update the content capture text changed event merge logic" into sc-dev
This commit is contained in:
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user