From 908126d4a7878a9c17f4e06afda57f7a995aadfc Mon Sep 17 00:00:00 2001 From: Feng Cao Date: Thu, 18 Jun 2020 12:57:07 -0700 Subject: [PATCH] Clear inline suggestions before onFinishInput * The problem with sending empty response to IME is that the IME may want to handle the following two cases differently: a) all suggestions are filtered out due to user typing: ime may want to immediately delete the existing suggestions to make place for other types of things, such as IME's own word completion or next word prediction. b) the current input connection is finished and a new connection will be created with the same field or a different field: ime may want to delay removing the suggestions so that if there is new inline suggestions coming soon after for the next connection, the UI transition can be smoothed out by skipping the gap of deleting the old suggestions and showing the new suggestions. * We used to rely on the IME impl to clear the suggestions when input is finished. That was done to give the IME the flexibility to smooth out the UI updates. Otherwise in case the input connection is finished and immediately started again on the same field, and there is another non-empty suggestion coming after short after, it would cause UI flicker. Because the suggsetion chips would disappear for a short moment and then appear again. * The previously implemented solution was to have the IME impl post a delayed deletion of the suggestions when onFinishInput is called. * In this patch, we get around this issue by synchronously clearing the inline suggestions right before the onFinishInput. Then the IME impl can post a callback to the main thread to do the actual delection. And in the callback it can check whether onFinishInput and onStartInput was called right before to determine whether it needs to delay the delection or delete immediately. * Also done in this patch is to clear existing inline suggestions, if any, before IME creating a new callback connection to the framework. Test: atest android.autofillservice.cts.inline Bug: 157515522 Change-Id: I6fd5d294cf8676a24b8576ea554824608672ce49 --- .../InlineSuggestionSession.java | 20 +++++++++++++++++++ .../InlineSuggestionSessionController.java | 1 + 2 files changed, 21 insertions(+) diff --git a/core/java/android/inputmethodservice/InlineSuggestionSession.java b/core/java/android/inputmethodservice/InlineSuggestionSession.java index 26197883c32fa..509cbe09df69a 100644 --- a/core/java/android/inputmethodservice/InlineSuggestionSession.java +++ b/core/java/android/inputmethodservice/InlineSuggestionSession.java @@ -38,6 +38,7 @@ import com.android.internal.view.IInlineSuggestionsResponseCallback; import com.android.internal.view.InlineSuggestionsRequestInfo; import java.lang.ref.WeakReference; +import java.util.Collections; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -58,6 +59,9 @@ import java.util.function.Supplier; class InlineSuggestionSession { private static final String TAG = "ImsInlineSuggestionSession"; + static final InlineSuggestionsResponse EMPTY_RESPONSE = new InlineSuggestionsResponse( + Collections.emptyList()); + @NonNull private final Handler mMainThreadHandler; @NonNull @@ -72,6 +76,10 @@ class InlineSuggestionSession { private final Supplier mHostInputTokenSupplier; @NonNull private final Consumer mResponseConsumer; + // Indicate whether the previous call to the mResponseConsumer is empty or not. If it hasn't + // been called yet, the value would be null. + @Nullable + private Boolean mPreviousResponseIsEmpty; /** @@ -142,6 +150,7 @@ class InlineSuggestionSession { @MainThread void invalidate() { if (mResponseCallback != null) { + consumeInlineSuggestionsResponse(EMPTY_RESPONSE); mResponseCallback.invalidate(); mResponseCallback = null; } @@ -188,6 +197,17 @@ class InlineSuggestionSession { if (DEBUG) { Log.d(TAG, "IME receives response: " + response.getInlineSuggestions().size()); } + consumeInlineSuggestionsResponse(response); + } + + @MainThread + void consumeInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { + boolean isResponseEmpty = response.getInlineSuggestions().isEmpty(); + if (isResponseEmpty && Boolean.TRUE.equals(mPreviousResponseIsEmpty)) { + // No-op if both the previous response and current response are empty. + return; + } + mPreviousResponseIsEmpty = isResponseEmpty; mResponseConsumer.accept(response); } diff --git a/core/java/android/inputmethodservice/InlineSuggestionSessionController.java b/core/java/android/inputmethodservice/InlineSuggestionSessionController.java index c9f9059bed4fc..8c0dd2a9bf59d 100644 --- a/core/java/android/inputmethodservice/InlineSuggestionSessionController.java +++ b/core/java/android/inputmethodservice/InlineSuggestionSessionController.java @@ -213,6 +213,7 @@ class InlineSuggestionSessionController { mImeInputViewStarted = false; mImeInputStarted = false; if (mSession != null && mSession.shouldSendImeStatus()) { + mSession.consumeInlineSuggestionsResponse(InlineSuggestionSession.EMPTY_RESPONSE); try { mSession.getRequestCallback().onInputMethodFinishInput(); } catch (RemoteException e) {