Send more IME events to autofill manager service.

* In IME side, wait for the input start before calling back to Autofill,
  rather than returning inline unsupported immediately.
* Also adds an InlineSuggestionManager to simplify code in the
  InputMethodService

Test: atest CtsAutoFillServiceTestCases
Test: atest CtsInputMethodTestCases
Bug: 151123764

Change-Id: I199925d77aa508f259e98a8929120aeb96015b57
This commit is contained in:
Feng Cao 2020-03-25 12:20:42 -07:00
parent 64d3ed6006
commit 97ec1c4dcc
6 changed files with 499 additions and 214 deletions

View File

@ -20,129 +20,148 @@ import static android.inputmethodservice.InputMethodService.DEBUG;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.BinderThread;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import com.android.internal.view.IInlineSuggestionsRequestCallback;
import com.android.internal.view.IInlineSuggestionsResponseCallback;
import com.android.internal.view.InlineSuggestionsRequestInfo;
import java.lang.ref.WeakReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Maintains an active inline suggestion session with the autofill manager service.
* Maintains an inline suggestion session with the autofill manager service.
*
* <p> Each session correspond to one request from the Autofill manager service to create an
* {@link InlineSuggestionsRequest}. It's responsible for calling back to the Autofill manager
* service with {@link InlineSuggestionsRequest} and receiving {@link InlineSuggestionsResponse}
* from it.
* <p>
* Each session corresponds to one {@link InlineSuggestionsRequest} and one {@link
* IInlineSuggestionsResponseCallback}, but there may be multiple invocations of the response
* callback for the same field or different fields in the same component.
* TODO(b/151123764): currently the session may receive responses for different views on the same
* screen, but we will fix it so each session corresponds to one view.
*
* <p>
* The data flow from IMS point of view is:
* Before calling {@link InputMethodService#onStartInputView(EditorInfo, boolean)}, call the {@link
* #notifyOnStartInputView(AutofillId)}
* ->
* [async] {@link IInlineSuggestionsRequestCallback#onInputMethodStartInputView(AutofillId)}
* --- process boundary ---
* ->
* {@link com.android.server.inputmethod.InputMethodManagerService
* .InlineSuggestionsRequestCallbackDecorator#onInputMethodStartInputView(AutofillId)}
* ->
* {@link com.android.server.autofill.InlineSuggestionSession
* .InlineSuggestionsRequestCallbackImpl#onInputMethodStartInputView(AutofillId)}
*
* <p>
* The data flow for {@link #notifyOnFinishInputView(AutofillId)} is similar.
* <p> All the methods are expected to be called from the main thread, to ensure thread safety.
*/
class InlineSuggestionSession {
private static final String TAG = "ImsInlineSuggestionSession";
private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
@NonNull
private final ComponentName mComponentName;
private final Handler mMainThreadHandler;
@NonNull
private final InlineSuggestionSessionController mInlineSuggestionSessionController;
@NonNull
private final InlineSuggestionsRequestInfo mRequestInfo;
@NonNull
private final IInlineSuggestionsRequestCallback mCallback;
@NonNull
private final InlineSuggestionsResponseCallbackImpl mResponseCallback;
@NonNull
private final Supplier<String> mClientPackageNameSupplier;
@NonNull
private final Supplier<AutofillId> mClientAutofillIdSupplier;
@NonNull
private final Supplier<InlineSuggestionsRequest> mRequestSupplier;
private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
@NonNull
private final Supplier<IBinder> mHostInputTokenSupplier;
@NonNull
private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
private volatile boolean mInvalidated = false;
InlineSuggestionSession(@NonNull ComponentName componentName,
/**
* Indicates whether {@link #makeInlineSuggestionRequestUncheck()} has been called or not,
* because it should only be called at most once.
*/
@Nullable
private boolean mCallbackInvoked = false;
@Nullable
private InlineSuggestionsResponseCallbackImpl mResponseCallback;
InlineSuggestionSession(@NonNull InlineSuggestionsRequestInfo requestInfo,
@NonNull IInlineSuggestionsRequestCallback callback,
@NonNull Supplier<String> clientPackageNameSupplier,
@NonNull Supplier<AutofillId> clientAutofillIdSupplier,
@NonNull Supplier<InlineSuggestionsRequest> requestSupplier,
@NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
@NonNull Supplier<IBinder> hostInputTokenSupplier,
@NonNull Consumer<InlineSuggestionsResponse> responseConsumer,
boolean inputViewStarted) {
mComponentName = componentName;
@NonNull InlineSuggestionSessionController inlineSuggestionSessionController,
@NonNull Handler mainThreadHandler) {
mRequestInfo = requestInfo;
mCallback = callback;
mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
mClientPackageNameSupplier = clientPackageNameSupplier;
mClientAutofillIdSupplier = clientAutofillIdSupplier;
mRequestSupplier = requestSupplier;
mHostInputTokenSupplier = hostInputTokenSupplier;
mResponseConsumer = responseConsumer;
makeInlineSuggestionsRequest(inputViewStarted);
mInlineSuggestionSessionController = inlineSuggestionSessionController;
mMainThreadHandler = mainThreadHandler;
}
void notifyOnStartInputView(AutofillId imeFieldId) {
if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
try {
mCallback.onInputMethodStartInputView(imeFieldId);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
}
@MainThread
InlineSuggestionsRequestInfo getRequestInfo() {
return mRequestInfo;
}
void notifyOnFinishInputView(AutofillId imeFieldId) {
if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
try {
mCallback.onInputMethodFinishInputView(imeFieldId);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
@MainThread
IInlineSuggestionsRequestCallback getRequestCallback() {
return mCallback;
}
/**
* Returns true if the session should send Ime status updates to Autofill.
*
* <p> The session only starts to send Ime status updates to Autofill after the sending back
* an {@link InlineSuggestionsRequest}.
*/
@MainThread
boolean shouldSendImeStatus() {
return mResponseCallback != null;
}
/**
* Returns true if {@link #makeInlineSuggestionRequestUncheck()} is called. It doesn't not
* necessarily mean an {@link InlineSuggestionsRequest} was sent, because it may call {@link
* IInlineSuggestionsRequestCallback#onInlineSuggestionsUnsupported()}.
*
* <p> The callback should be invoked at most once for each session.
*/
@MainThread
boolean isCallbackInvoked() {
return mCallbackInvoked;
}
/**
* Invalidates the current session so it doesn't process any further {@link
* InlineSuggestionsResponse} from Autofill.
*
* <p> This method should be called when the session is de-referenced from the {@link
* InlineSuggestionSessionController}.
*/
@MainThread
void invalidate() {
if (mResponseCallback != null) {
mResponseCallback.invalidate();
mResponseCallback = null;
}
}
/**
* This needs to be called before creating a new session, such that the later response callbacks
* will be discarded.
* Gets the {@link InlineSuggestionsRequest} from IME and send it back to the Autofill if it's
* not null.
*
* <p>Calling this method implies that the input is started on the view corresponding to the
* session.
*/
void invalidateSession() {
mInvalidated = true;
}
/**
* Sends an {@link InlineSuggestionsRequest} obtained from {@cocde supplier} to the current
* Autofill Session through
* {@link IInlineSuggestionsRequestCallback#onInlineSuggestionsRequest}.
*/
private void makeInlineSuggestionsRequest(boolean inputViewStarted) {
@MainThread
void makeInlineSuggestionRequestUncheck() {
if (mCallbackInvoked) {
return;
}
try {
final InlineSuggestionsRequest request = mRequestSupplier.get();
final InlineSuggestionsRequest request = mRequestSupplier.apply(
mRequestInfo.getUiExtras());
if (request == null) {
if (DEBUG) {
Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request");
@ -150,37 +169,19 @@ class InlineSuggestionSession {
mCallback.onInlineSuggestionsUnsupported();
} else {
request.setHostInputToken(mHostInputTokenSupplier.get());
mCallback.onInlineSuggestionsRequest(request, mResponseCallback,
mClientAutofillIdSupplier.get(), inputViewStarted);
mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this);
mCallback.onInlineSuggestionsRequest(request, mResponseCallback);
}
} catch (RemoteException e) {
Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e);
}
mCallbackInvoked = true;
}
private void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId,
@MainThread
void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId,
@NonNull InlineSuggestionsResponse response) {
if (mInvalidated) {
if (DEBUG) {
Log.d(TAG, "handleOnInlineSuggestionsResponse() called on invalid session");
}
return;
}
// The IME doesn't have information about the virtual view id for the child views in the
// web view, so we are only comparing the parent view id here. This means that for cases
// where there are two input fields in the web view, they will have the same view id
// (although different virtual child id), and we will not be able to distinguish them.
final AutofillId imeClientFieldId = mClientAutofillIdSupplier.get();
if (!mComponentName.getPackageName().equals(mClientPackageNameSupplier.get())
|| imeClientFieldId == null
|| fieldId.getViewId() != imeClientFieldId.getViewId()) {
if (DEBUG) {
Log.d(TAG,
"handleOnInlineSuggestionsResponse() called on the wrong package/field "
+ "name: " + mComponentName.getPackageName() + " v.s. "
+ mClientPackageNameSupplier.get() + ", " + fieldId + " v.s. "
+ mClientAutofillIdSupplier.get());
}
if (!mInlineSuggestionSessionController.match(fieldId)) {
return;
}
if (DEBUG) {
@ -192,23 +193,31 @@ class InlineSuggestionSession {
/**
* Internal implementation of {@link IInlineSuggestionsResponseCallback}.
*/
static final class InlineSuggestionsResponseCallbackImpl
extends IInlineSuggestionsResponseCallback.Stub {
private final WeakReference<InlineSuggestionSession> mInlineSuggestionSession;
private static final class InlineSuggestionsResponseCallbackImpl extends
IInlineSuggestionsResponseCallback.Stub {
private final WeakReference<InlineSuggestionSession> mSession;
private volatile boolean mInvalid = false;
private InlineSuggestionsResponseCallbackImpl(
InlineSuggestionSession inlineSuggestionSession) {
mInlineSuggestionSession = new WeakReference<>(inlineSuggestionSession);
private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) {
mSession = new WeakReference<>(session);
}
void invalidate() {
mInvalid = true;
}
@BinderThread
@Override
public void onInlineSuggestionsResponse(AutofillId fieldId,
InlineSuggestionsResponse response) {
final InlineSuggestionSession session = mInlineSuggestionSession.get();
if (mInvalid) {
return;
}
final InlineSuggestionSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
InlineSuggestionSession::handleOnInlineSuggestionsResponse, session,
fieldId, response));
session.mMainThreadHandler.sendMessage(
obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse,
session, fieldId, response));
}
}
}

View File

@ -0,0 +1,261 @@
/*
* Copyright (C) 2020 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.inputmethodservice;
import static android.inputmethodservice.InputMethodService.DEBUG;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import com.android.internal.view.IInlineSuggestionsRequestCallback;
import com.android.internal.view.InlineSuggestionsRequestInfo;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Manages the interaction with the autofill manager service for the inline suggestion sessions.
*
* <p>
* The class maintains the inline suggestion session with the autofill service. There is at most one
* active inline suggestion session at any given time.
*
* <p>
* The class receives the IME status change events (input start/finish, input view start/finish, and
* show input requested result), and send them through IPC to the {@link
* com.android.server.inputmethod.InputMethodManagerService}, which sends them to {@link
* com.android.server.autofill.InlineSuggestionSession} in the Autofill manager service. If there is
* no open inline suggestion session, no event will be send to autofill manager service.
*
* <p>
* All the methods are expected to be called from the main thread, to ensure thread safety.
*/
class InlineSuggestionSessionController {
private static final String TAG = "InlineSuggestionSessionController";
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true);
@NonNull
private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier;
@NonNull
private final Supplier<IBinder> mHostInputTokenSupplier;
@NonNull
private final Consumer<InlineSuggestionsResponse> mResponseConsumer;
/* The following variables track the IME status */
@Nullable
private String mImeClientPackageName;
@Nullable
private AutofillId mImeClientFieldId;
private boolean mImeInputStarted;
private boolean mImeInputViewStarted;
@Nullable
private InlineSuggestionSession mSession;
InlineSuggestionSessionController(
@NonNull Function<Bundle, InlineSuggestionsRequest> requestSupplier,
@NonNull Supplier<IBinder> hostInputTokenSupplier,
@NonNull Consumer<InlineSuggestionsResponse> responseConsumer) {
mRequestSupplier = requestSupplier;
mHostInputTokenSupplier = hostInputTokenSupplier;
mResponseConsumer = responseConsumer;
}
/**
* Called upon IME receiving a create inline suggestion request. Must be called in the main
* thread to ensure thread safety.
*/
@MainThread
void onMakeInlineSuggestionsRequest(@NonNull InlineSuggestionsRequestInfo requestInfo,
@NonNull IInlineSuggestionsRequestCallback callback) {
if (DEBUG) Log.d(TAG, "onMakeInlineSuggestionsRequest: " + requestInfo);
// Creates a new session for the new create request from Autofill.
if (mSession != null) {
mSession.invalidate();
}
mSession = new InlineSuggestionSession(requestInfo, callback, mRequestSupplier,
mHostInputTokenSupplier, mResponseConsumer, this, mMainThreadHandler);
// If the input is started on the same view, then initiate the callback to the Autofill.
// Otherwise wait for the input to start.
if (mImeInputStarted && match(mSession.getRequestInfo())) {
mSession.makeInlineSuggestionRequestUncheck();
// ... then update the Autofill whether the input view is started.
if (mImeInputViewStarted) {
try {
mSession.getRequestCallback().onInputMethodStartInputView();
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
}
}
}
}
/**
* Called from IME main thread before calling {@link InputMethodService#onStartInput(EditorInfo,
* boolean)}. This method should be quick as it makes a unblocking IPC.
*/
@MainThread
void notifyOnStartInput(@Nullable String imeClientPackageName,
@Nullable AutofillId imeFieldId) {
if (DEBUG) Log.d(TAG, "notifyOnStartInput: " + imeClientPackageName + ", " + imeFieldId);
if (imeClientPackageName == null || imeFieldId == null) {
return;
}
mImeInputStarted = true;
mImeClientPackageName = imeClientPackageName;
mImeClientFieldId = imeFieldId;
if (mSession != null) {
// Initiates the callback to Autofill if there is a pending matching session.
// Otherwise updates the session with the Ime status.
if (!mSession.isCallbackInvoked() && match(mSession.getRequestInfo())) {
mSession.makeInlineSuggestionRequestUncheck();
} else if (mSession.shouldSendImeStatus()) {
try {
mSession.getRequestCallback().onInputMethodStartInput(mImeClientFieldId);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodStartInput() remote exception:" + e);
}
}
}
}
/**
* Called from IME main thread after getting results from
* {@link InputMethodService#dispatchOnShowInputRequested(int,
* boolean)}. This method should be quick as it makes a unblocking IPC.
*/
@MainThread
void notifyOnShowInputRequested(boolean requestResult) {
if (DEBUG) Log.d(TAG, "notifyShowInputRequested");
if (mSession != null && mSession.shouldSendImeStatus()) {
try {
mSession.getRequestCallback().onInputMethodShowInputRequested(requestResult);
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodShowInputRequested() remote exception:" + e);
}
}
}
/**
* Called from IME main thread before calling
* {@link InputMethodService#onStartInputView(EditorInfo,
* boolean)} . This method should be quick as it makes a unblocking IPC.
*/
@MainThread
void notifyOnStartInputView() {
if (DEBUG) Log.d(TAG, "notifyOnStartInputView");
mImeInputViewStarted = true;
if (mSession != null && mSession.shouldSendImeStatus()) {
try {
mSession.getRequestCallback().onInputMethodStartInputView();
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodStartInputView() remote exception:" + e);
}
}
}
/**
* Called from IME main thread before calling
* {@link InputMethodService#onFinishInputView(boolean)}.
* This method should be quick as it makes a unblocking IPC.
*/
@MainThread
void notifyOnFinishInputView() {
if (DEBUG) Log.d(TAG, "notifyOnFinishInputView");
mImeInputViewStarted = false;
if (mSession != null && mSession.shouldSendImeStatus()) {
try {
mSession.getRequestCallback().onInputMethodFinishInputView();
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodFinishInputView() remote exception:" + e);
}
}
}
/**
* Called from IME main thread before calling {@link InputMethodService#onFinishInput()}. This
* method should be quick as it makes a unblocking IPC.
*/
@MainThread
void notifyOnFinishInput() {
if (DEBUG) Log.d(TAG, "notifyOnFinishInput");
mImeClientPackageName = null;
mImeClientFieldId = null;
mImeInputViewStarted = false;
mImeInputStarted = false;
if (mSession != null && mSession.shouldSendImeStatus()) {
try {
mSession.getRequestCallback().onInputMethodFinishInput();
} catch (RemoteException e) {
Log.w(TAG, "onInputMethodFinishInput() remote exception:" + e);
}
}
}
/**
* Returns true if the current Ime focused field matches the session {@code requestInfo}.
*/
@MainThread
boolean match(@Nullable InlineSuggestionsRequestInfo requestInfo) {
return match(requestInfo, mImeClientPackageName, mImeClientFieldId);
}
/**
* Returns true if the current Ime focused field matches the {@code autofillId}.
*/
@MainThread
boolean match(@Nullable AutofillId autofillId) {
return match(autofillId, mImeClientFieldId);
}
private static boolean match(
@Nullable InlineSuggestionsRequestInfo inlineSuggestionsRequestInfo,
@Nullable String imeClientPackageName, @Nullable AutofillId imeClientFieldId) {
if (inlineSuggestionsRequestInfo == null || imeClientPackageName == null
|| imeClientFieldId == null) {
return false;
}
return inlineSuggestionsRequestInfo.getComponentName().getPackageName().equals(
imeClientPackageName) && match(inlineSuggestionsRequestInfo.getAutofillId(),
imeClientFieldId);
}
private static boolean match(@Nullable AutofillId autofillId,
@Nullable AutofillId imeClientFieldId) {
// The IME doesn't have information about the virtual view id for the child views in the
// web view, so we are only comparing the parent view id here. This means that for cases
// where there are two input fields in the web view, they will have the same view id
// (although different virtual child id), and we will not be able to distinguish them.
return autofillId != null && imeClientFieldId != null
&& autofillId.getViewId() == imeClientFieldId.getViewId();
}
}

View File

@ -49,7 +49,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.provider.Settings;
@ -74,7 +73,6 @@ import android.view.WindowInsets;
import android.view.WindowInsets.Side;
import android.view.WindowManager;
import android.view.animation.AnimationUtils;
import android.view.autofill.AutofillId;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
@ -445,18 +443,7 @@ public class InputMethodService extends AbstractInputMethodService {
final Insets mTmpInsets = new Insets();
final int[] mTmpLocation = new int[2];
/**
* We use a separate {@code mInlineLock} to make sure {@code mInlineSuggestionSession} is
* only accessed synchronously. Although when the lock is introduced, all the calls are from
* the main thread so the lock is not really necessarily (but for the same reason it also
* doesn't hurt), it's still being added as a safety guard to make sure in the future we
* don't add more code causing race condition when updating the {@code
* mInlineSuggestionSession}.
*/
private final Object mInlineLock = new Object();
@GuardedBy("mInlineLock")
@Nullable
private InlineSuggestionSession mInlineSuggestionSession;
private InlineSuggestionSessionController mInlineSuggestionSessionController;
private boolean mAutomotiveHideNavBarForKeyboard;
private boolean mIsAutomotive;
@ -554,7 +541,7 @@ public class InputMethodService extends AbstractInputMethodService {
if (DEBUG) {
Log.d(TAG, "InputMethodService received onCreateInlineSuggestionsRequest()");
}
handleOnCreateInlineSuggestionsRequest(requestInfo, cb);
mInlineSuggestionSessionController.onMakeInlineSuggestionsRequest(requestInfo, cb);
}
/**
@ -823,47 +810,6 @@ public class InputMethodService extends AbstractInputMethodService {
return false;
}
@MainThread
private void handleOnCreateInlineSuggestionsRequest(
@NonNull InlineSuggestionsRequestInfo requestInfo,
@NonNull IInlineSuggestionsRequestCallback callback) {
if (!mInputStarted) {
try {
Log.w(TAG, "onStartInput() not called yet");
callback.onInlineSuggestionsUnsupported();
} catch (RemoteException e) {
Log.w(TAG, "Failed to call onInlineSuggestionsUnsupported.", e);
}
return;
}
synchronized (mInlineLock) {
if (mInlineSuggestionSession != null) {
mInlineSuggestionSession.invalidateSession();
}
mInlineSuggestionSession = new InlineSuggestionSession(requestInfo.getComponentName(),
callback, this::getEditorInfoPackageName, this::getEditorInfoAutofillId,
() -> onCreateInlineSuggestionsRequest(requestInfo.getUiExtras()),
this::getHostInputToken, this::onInlineSuggestionsResponse, mInputViewStarted);
}
}
@Nullable
private String getEditorInfoPackageName() {
if (mInputEditorInfo != null) {
return mInputEditorInfo.packageName;
}
return null;
}
@Nullable
private AutofillId getEditorInfoAutofillId() {
if (mInputEditorInfo != null) {
return mInputEditorInfo.autofillId;
}
return null;
}
/**
* Returns the {@link IBinder} input token from the host view root.
*/
@ -1269,6 +1215,10 @@ public class InputMethodService extends AbstractInputMethodService {
initViews();
mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
mInlineSuggestionSessionController = new InlineSuggestionSessionController(
this::onCreateInlineSuggestionsRequest, this::getHostInputToken,
this::onInlineSuggestionsResponse);
}
/**
@ -2112,6 +2062,7 @@ public class InputMethodService extends AbstractInputMethodService {
*/
private boolean dispatchOnShowInputRequested(int flags, boolean configChange) {
final boolean result = onShowInputRequested(flags, configChange);
mInlineSuggestionSessionController.notifyOnShowInputRequested(result);
if (result) {
mShowInputFlags = flags;
} else {
@ -2211,11 +2162,7 @@ public class InputMethodService extends AbstractInputMethodService {
if (!mInputViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
mInputViewStarted = true;
synchronized (mInlineLock) {
if (mInlineSuggestionSession != null) {
mInlineSuggestionSession.notifyOnStartInputView(getEditorInfoAutofillId());
}
}
mInlineSuggestionSessionController.notifyOnStartInputView();
onStartInputView(mInputEditorInfo, false);
}
} else if (!mCandidatesViewStarted) {
@ -2256,11 +2203,7 @@ public class InputMethodService extends AbstractInputMethodService {
private void finishViews(boolean finishingInput) {
if (mInputViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onFinishInputView");
synchronized (mInlineLock) {
if (mInlineSuggestionSession != null) {
mInlineSuggestionSession.notifyOnFinishInputView(getEditorInfoAutofillId());
}
}
mInlineSuggestionSessionController.notifyOnFinishInputView();
onFinishInputView(finishingInput);
} else if (mCandidatesViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onFinishCandidatesView");
@ -2355,6 +2298,7 @@ public class InputMethodService extends AbstractInputMethodService {
if (DEBUG) Log.v(TAG, "CALL: doFinishInput");
finishViews(true /* finishingInput */);
if (mInputStarted) {
mInlineSuggestionSessionController.notifyOnFinishInput();
if (DEBUG) Log.v(TAG, "CALL: onFinishInput");
onFinishInput();
}
@ -2371,17 +2315,16 @@ public class InputMethodService extends AbstractInputMethodService {
mStartedInputConnection = ic;
mInputEditorInfo = attribute;
initialize();
mInlineSuggestionSessionController.notifyOnStartInput(
attribute == null ? null : attribute.packageName,
attribute == null ? null : attribute.autofillId);
if (DEBUG) Log.v(TAG, "CALL: onStartInput");
onStartInput(attribute, restarting);
if (mDecorViewVisible) {
if (mShowInputRequested) {
if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
mInputViewStarted = true;
synchronized (mInlineLock) {
if (mInlineSuggestionSession != null) {
mInlineSuggestionSession.notifyOnStartInputView(getEditorInfoAutofillId());
}
}
mInlineSuggestionSessionController.notifyOnStartInputView();
onStartInputView(mInputEditorInfo, restarting);
startExtractingText(true);
} else if (mCandidatesVisibility == View.VISIBLE) {

View File

@ -26,10 +26,36 @@ import com.android.internal.view.IInlineSuggestionsResponseCallback;
* {@hide}
*/
oneway interface IInlineSuggestionsRequestCallback {
// Indicates that the current IME does not support inline suggestion.
void onInlineSuggestionsUnsupported();
// Sends the inline suggestions request from IME to Autofill. Calling this method indicates
// that the IME input is started on the view corresponding to the request.
void onInlineSuggestionsRequest(in InlineSuggestionsRequest request,
in IInlineSuggestionsResponseCallback callback, in AutofillId imeFieldId,
boolean inputViewStarted);
void onInputMethodStartInputView(in AutofillId imeFieldId);
void onInputMethodFinishInputView(in AutofillId imeFieldId);
in IInlineSuggestionsResponseCallback callback);
// Signals that {@link android.inputmethodservice.InputMethodService
// #onStartInput(EditorInfo, boolean)} is called on the given focused field.
void onInputMethodStartInput(in AutofillId imeFieldId);
// Signals that {@link android.inputmethodservice.InputMethodService
// #dispatchOnShowInputRequested(int, boolean)} is called and shares the call result.
// The true value of {@code requestResult} means the IME is about to be shown, while
// false value means the IME will not be shown.
void onInputMethodShowInputRequested(boolean requestResult);
// Signals that {@link android.inputmethodservice.InputMethodService
// #onStartInputView(EditorInfo, boolean)} is called on the field specified by the earlier
// {@link #onInputMethodStartInput(AutofillId)}.
void onInputMethodStartInputView();
// Signals that {@link android.inputmethodservice.InputMethodService
// #onFinishInputView(boolean)} is called on the field specified by the earlier
// {@link #onInputMethodStartInput(AutofillId)}.
void onInputMethodFinishInputView();
// Signals that {@link android.inputmethodservice.InputMethodService
// #onFinishInput()} is called on the field specified by the earlier
// {@link #onInputMethodStartInput(AutofillId)}.
void onInputMethodFinishInput();
}

View File

@ -113,9 +113,16 @@ final class InlineSuggestionSession {
mLock = lock;
mImeStatusListener = new ImeStatusListener() {
@Override
public void onInputMethodStartInputView(AutofillId imeFieldId) {
public void onInputMethodStartInput(AutofillId imeFieldId) {
synchronized (mLock) {
mImeFieldId = imeFieldId;
mImeInputViewStarted = false;
}
}
@Override
public void onInputMethodStartInputView() {
synchronized (mLock) {
mImeInputViewStarted = true;
AutofillResponse pendingAutofillResponse = mPendingAutofillResponse;
if (pendingAutofillResponse != null
@ -129,12 +136,18 @@ final class InlineSuggestionSession {
}
@Override
public void onInputMethodFinishInputView(AutofillId imeFieldId) {
public void onInputMethodFinishInputView() {
synchronized (mLock) {
mImeFieldId = imeFieldId;
mImeInputViewStarted = false;
}
}
@Override
public void onInputMethodFinishInput() {
synchronized (mLock) {
mImeFieldId = null;
}
}
};
}
@ -155,7 +168,7 @@ final class InlineSuggestionSession {
mInputMethodManagerInternal.onCreateInlineSuggestionsRequest(
mUserId,
new InlineSuggestionsRequestInfo(mComponentName, autofillId, new Bundle()),
new InlineSuggestionsRequestCallbackImpl(mPendingImeResponse,
new InlineSuggestionsRequestCallbackImpl(autofillId, mPendingImeResponse,
mImeStatusListener, requestConsumer, mHandler, mLock));
}
}
@ -244,6 +257,7 @@ final class InlineSuggestionSession {
extends IInlineSuggestionsRequestCallback.Stub {
private final Object mLock;
private final AutofillId mAutofillId;
@GuardedBy("mLock")
private final CompletableFuture<ImeResponse> mResponse;
@GuardedBy("mLock")
@ -252,10 +266,12 @@ final class InlineSuggestionSession {
private final Handler mHandler;
private final Runnable mTimeoutCallback;
private InlineSuggestionsRequestCallbackImpl(CompletableFuture<ImeResponse> response,
private InlineSuggestionsRequestCallbackImpl(AutofillId autofillId,
CompletableFuture<ImeResponse> response,
ImeStatusListener imeStatusListener,
Consumer<InlineSuggestionsRequest> requestConsumer,
Handler handler, Object lock) {
mAutofillId = autofillId;
mResponse = response;
mImeStatusListener = imeStatusListener;
mRequestConsumer = requestConsumer;
@ -290,18 +306,9 @@ final class InlineSuggestionSession {
@BinderThread
@Override
public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
IInlineSuggestionsResponseCallback callback, AutofillId imeFieldId,
boolean inputViewStarted) {
if (sDebug) {
Log.d(TAG,
"onInlineSuggestionsRequest() received: " + request + ", inputViewStarted="
+ inputViewStarted + ", imeFieldId=" + imeFieldId);
}
if (inputViewStarted) {
mImeStatusListener.onInputMethodStartInputView(imeFieldId);
} else {
mImeStatusListener.onInputMethodFinishInputView(imeFieldId);
}
IInlineSuggestionsResponseCallback callback) {
if (sDebug) Log.d(TAG, "onInlineSuggestionsRequest() received: " + request);
mImeStatusListener.onInputMethodStartInput(mAutofillId);
if (request != null && callback != null) {
completeIfNot(new ImeResponse(request, callback));
} else {
@ -309,25 +316,50 @@ final class InlineSuggestionSession {
}
}
@BinderThread
@Override
public void onInputMethodStartInputView(AutofillId imeFieldId) {
if (sDebug) Log.d(TAG, "onInputMethodStartInputView() received on " + imeFieldId);
mImeStatusListener.onInputMethodStartInputView(imeFieldId);
public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
if (sDebug) Log.d(TAG, "onInputMethodStartInput() received on " + imeFieldId);
mImeStatusListener.onInputMethodStartInput(imeFieldId);
}
@Override
public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
if (sDebug) {
Log.d(TAG, "onInputMethodShowInputRequested() received: " + requestResult);
}
// TODO(b/151123764): use this signal to adjust the timeout on Autofill side waiting for
// IME to show.
}
@BinderThread
@Override
public void onInputMethodFinishInputView(AutofillId imeFieldId) {
if (sDebug) Log.d(TAG, "onInputMethodFinishInputView() received on " + imeFieldId);
mImeStatusListener.onInputMethodFinishInputView(imeFieldId);
public void onInputMethodStartInputView() {
if (sDebug) Log.d(TAG, "onInputMethodStartInputView() received");
mImeStatusListener.onInputMethodStartInputView();
}
@BinderThread
@Override
public void onInputMethodFinishInputView() {
if (sDebug) Log.d(TAG, "onInputMethodFinishInputView() received");
mImeStatusListener.onInputMethodFinishInputView();
}
@Override
public void onInputMethodFinishInput() throws RemoteException {
if (sDebug) Log.d(TAG, "onInputMethodFinishInput() received");
mImeStatusListener.onInputMethodFinishInput();
}
}
private interface ImeStatusListener {
void onInputMethodStartInputView(AutofillId imeFieldId);
void onInputMethodStartInput(AutofillId imeFieldId);
void onInputMethodFinishInputView(AutofillId imeFieldId);
void onInputMethodStartInputView();
void onInputMethodFinishInputView();
void onInputMethodFinishInput();
}
/**

View File

@ -2042,8 +2042,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
@Override
public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
IInlineSuggestionsResponseCallback callback, AutofillId imeFieldId,
boolean inputViewStarted)
IInlineSuggestionsResponseCallback callback)
throws RemoteException {
if (!mImePackageName.equals(request.getHostPackageName())) {
throw new SecurityException(
@ -2053,17 +2052,32 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
request.setHostDisplayId(mImeDisplayId);
mImms.setCurHostInputToken(mImeToken, request.getHostInputToken());
mCallback.onInlineSuggestionsRequest(request, callback, imeFieldId, inputViewStarted);
mCallback.onInlineSuggestionsRequest(request, callback);
}
@Override
public void onInputMethodStartInputView(AutofillId imeFieldId) throws RemoteException {
mCallback.onInputMethodStartInputView(imeFieldId);
public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
mCallback.onInputMethodStartInput(imeFieldId);
}
@Override
public void onInputMethodFinishInputView(AutofillId imeFieldId) throws RemoteException {
mCallback.onInputMethodFinishInputView(imeFieldId);
public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
mCallback.onInputMethodShowInputRequested(requestResult);
}
@Override
public void onInputMethodStartInputView() throws RemoteException {
mCallback.onInputMethodStartInputView();
}
@Override
public void onInputMethodFinishInputView() throws RemoteException {
mCallback.onInputMethodFinishInputView();
}
@Override
public void onInputMethodFinishInput() throws RemoteException {
mCallback.onInputMethodFinishInput();
}
}