diff options
| author | Xin Li <delphij@google.com> | 2020-09-01 20:04:08 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-09-01 20:04:08 +0000 |
| commit | ff2ffe48acb657c959cae265529a7290013c7db8 (patch) | |
| tree | 6d05301b67632d384a392190306ecb088c896cb2 /core/java/android/inputmethodservice | |
| parent | b57f32e68ab2f57fd4ade254d524a4efce9ad30f (diff) | |
| parent | 628590d7ec80e10a3fc24b1c18a1afb55cca10a8 (diff) | |
Merge "Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)" into stage-aosp-master
Diffstat (limited to 'core/java/android/inputmethodservice')
8 files changed, 879 insertions, 74 deletions
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index b0fca006d1e2..d7ca63a54987 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -260,4 +260,10 @@ public abstract class AbstractInputMethodService extends Service */ public void notifyUserActionIfNecessary() { } + + /** @hide */ + @Override + public final boolean isUiContext() { + return true; + } } diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index 9327b241c6c5..e9de27456f97 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -53,6 +53,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_FINISH_SESSION = 110; private static final int DO_VIEW_CLICKED = 115; private static final int DO_NOTIFY_IME_HIDDEN = 120; + private static final int DO_REMOVE_IME_SURFACE = 130; @UnsupportedAppUsage HandlerCaller mCaller; @@ -136,6 +137,10 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mInputMethodSession.notifyImeHidden(); return; } + case DO_REMOVE_IME_SURFACE: { + mInputMethodSession.removeImeSurface(); + return; + } } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -184,6 +189,11 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub } @Override + public void removeImeSurface() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_IME_SURFACE)); + } + + @Override public void updateCursor(Rect newCursor) { mCaller.executeOrSendMessage( mCaller.obtainMessageO(DO_UPDATE_CURSOR, newCursor)); diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index 69b93b19fc41..a298c856a0fb 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -18,6 +18,7 @@ package android.inputmethodservice; import android.annotation.BinderThread; import android.annotation.MainThread; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.PackageManager; @@ -37,12 +38,15 @@ import android.view.inputmethod.InputMethodSession; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethod; import com.android.internal.view.IInputMethodSession; import com.android.internal.view.IInputSessionCallback; +import com.android.internal.view.InlineSuggestionsRequestInfo; import com.android.internal.view.InputConnectionWrapper; import java.io.FileDescriptor; @@ -50,7 +54,6 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; /** * Implements the internal IInputMethod interface to convert incoming calls @@ -72,6 +75,7 @@ class IInputMethodWrapper extends IInputMethod.Stub private static final int DO_SHOW_SOFT_INPUT = 60; private static final int DO_HIDE_SOFT_INPUT = 70; private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80; + private static final int DO_CREATE_INLINE_SUGGESTIONS_REQUEST = 90; final WeakReference<AbstractInputMethodService> mTarget; final Context mContext; @@ -87,12 +91,13 @@ class IInputMethodWrapper extends IInputMethod.Stub * * <p>This field must be set and cleared only from the binder thread(s), where the system * guarantees that {@link #bindInput(InputBinding)}, - * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean)}, and + * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean, boolean)}, and * {@link #unbindInput()} are called with the same order as the original calls * in {@link com.android.server.inputmethod.InputMethodManagerService}. * See {@link IBinder#FLAG_ONEWAY} for detailed semantics.</p> */ - AtomicBoolean mIsUnbindIssued = null; + @Nullable + CancellationGroup mCancellationGroup = null; // NOTE: we should have a cache of these. static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { @@ -184,11 +189,11 @@ class IInputMethodWrapper extends IInputMethod.Stub final IBinder startInputToken = (IBinder) args.arg1; final IInputContext inputContext = (IInputContext) args.arg2; final EditorInfo info = (EditorInfo) args.arg3; - final AtomicBoolean isUnbindIssued = (AtomicBoolean) args.arg4; + final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4; SomeArgs moreArgs = (SomeArgs) args.arg5; final InputConnection ic = inputContext != null ? new InputConnectionWrapper( - mTarget, inputContext, moreArgs.argi3, isUnbindIssued) + mTarget, inputContext, moreArgs.argi3, cancellationGroup) : null; info.makeCompatible(mTargetSdkVersion); inputMethod.dispatchStartInputWithToken( @@ -216,15 +221,31 @@ class IInputMethodWrapper extends IInputMethod.Stub case DO_REVOKE_SESSION: inputMethod.revokeSession((InputMethodSession)msg.obj); return; - case DO_SHOW_SOFT_INPUT: - inputMethod.showSoftInput(msg.arg1, (ResultReceiver)msg.obj); + case DO_SHOW_SOFT_INPUT: { + final SomeArgs args = (SomeArgs)msg.obj; + inputMethod.showSoftInputWithToken( + msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1); + args.recycle(); return; - case DO_HIDE_SOFT_INPUT: - inputMethod.hideSoftInput(msg.arg1, (ResultReceiver)msg.obj); + } + case DO_HIDE_SOFT_INPUT: { + final SomeArgs args = (SomeArgs) msg.obj; + inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2, + (IBinder) args.arg1); + args.recycle(); return; + } case DO_CHANGE_INPUTMETHOD_SUBTYPE: inputMethod.changeInputMethodSubtype((InputMethodSubtype)msg.obj); return; + case DO_CREATE_INLINE_SUGGESTIONS_REQUEST: + final SomeArgs args = (SomeArgs) msg.obj; + inputMethod.onCreateInlineSuggestionsRequest( + (InlineSuggestionsRequestInfo) args.arg1, + (IInlineSuggestionsRequestCallback) args.arg2); + args.recycle(); + return; + } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -267,16 +288,24 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override + public void onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo, + IInlineSuggestionsRequestCallback cb) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_CREATE_INLINE_SUGGESTIONS_REQUEST, requestInfo, cb)); + } + + @BinderThread + @Override public void bindInput(InputBinding binding) { - if (mIsUnbindIssued != null) { + if (mCancellationGroup != null) { Log.e(TAG, "bindInput must be paired with unbindInput."); } - mIsUnbindIssued = new AtomicBoolean(); + mCancellationGroup = new CancellationGroup(); // This IInputContext is guaranteed to implement all the methods. final int missingMethodFlags = 0; InputConnection ic = new InputConnectionWrapper(mTarget, IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags, - mIsUnbindIssued); + mCancellationGroup); InputBinding nu = new InputBinding(ic, binding); mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); } @@ -284,10 +313,10 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override public void unbindInput() { - if (mIsUnbindIssued != null) { + if (mCancellationGroup != null) { // Signal the flag then forget it. - mIsUnbindIssued.set(true); - mIsUnbindIssued = null; + mCancellationGroup.cancelAll(); + mCancellationGroup = null; } else { Log.e(TAG, "unbindInput must be paired with bindInput."); } @@ -299,16 +328,16 @@ class IInputMethodWrapper extends IInputMethod.Stub public void startInput(IBinder startInputToken, IInputContext inputContext, @InputConnectionInspector.MissingMethodFlags final int missingMethods, EditorInfo attribute, boolean restarting, boolean shouldPreRenderIme) { - if (mIsUnbindIssued == null) { + if (mCancellationGroup == null) { Log.e(TAG, "startInput must be called after bindInput."); - mIsUnbindIssued = new AtomicBoolean(); + mCancellationGroup = new CancellationGroup(); } SomeArgs args = SomeArgs.obtain(); args.argi1 = restarting ? 1 : 0; args.argi2 = shouldPreRenderIme ? 1 : 0; args.argi3 = missingMethods; - mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO( - DO_START_INPUT, startInputToken, inputContext, attribute, mIsUnbindIssued, args)); + mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOO(DO_START_INPUT, startInputToken, + inputContext, attribute, mCancellationGroup, args)); } @BinderThread @@ -353,16 +382,16 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override - public void showSoftInput(int flags, ResultReceiver resultReceiver) { - mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_SHOW_SOFT_INPUT, - flags, resultReceiver)); + public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT, + flags, showInputToken, resultReceiver)); } @BinderThread @Override - public void hideSoftInput(int flags, ResultReceiver resultReceiver) { - mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_HIDE_SOFT_INPUT, - flags, resultReceiver)); + public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT, + flags, hideInputToken, resultReceiver)); } @BinderThread diff --git a/core/java/android/inputmethodservice/InlineSuggestionSession.java b/core/java/android/inputmethodservice/InlineSuggestionSession.java new file mode 100644 index 000000000000..90d0ff0a5026 --- /dev/null +++ b/core/java/android/inputmethodservice/InlineSuggestionSession.java @@ -0,0 +1,250 @@ +/* + * 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 static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.BinderThread; +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.RemoteException; +import android.util.Log; +import android.view.autofill.AutofillId; +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.Collections; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 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> + * 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> All the methods are expected to be called from the main thread, to ensure thread safety. + */ +class InlineSuggestionSession { + private static final String TAG = "ImsInlineSuggestionSession"; + + static final InlineSuggestionsResponse EMPTY_RESPONSE = new InlineSuggestionsResponse( + Collections.emptyList()); + + @NonNull + private final Handler mMainThreadHandler; + @NonNull + private final InlineSuggestionSessionController mInlineSuggestionSessionController; + @NonNull + private final InlineSuggestionsRequestInfo mRequestInfo; + @NonNull + private final IInlineSuggestionsRequestCallback mCallback; + @NonNull + private final Function<Bundle, InlineSuggestionsRequest> mRequestSupplier; + @NonNull + private final Supplier<IBinder> mHostInputTokenSupplier; + @NonNull + private final Consumer<InlineSuggestionsResponse> 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; + + + /** + * 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 Function<Bundle, InlineSuggestionsRequest> requestSupplier, + @NonNull Supplier<IBinder> hostInputTokenSupplier, + @NonNull Consumer<InlineSuggestionsResponse> responseConsumer, + @NonNull InlineSuggestionSessionController inlineSuggestionSessionController, + @NonNull Handler mainThreadHandler) { + mRequestInfo = requestInfo; + mCallback = callback; + mRequestSupplier = requestSupplier; + mHostInputTokenSupplier = hostInputTokenSupplier; + mResponseConsumer = responseConsumer; + mInlineSuggestionSessionController = inlineSuggestionSessionController; + mMainThreadHandler = mainThreadHandler; + } + + @MainThread + InlineSuggestionsRequestInfo getRequestInfo() { + return mRequestInfo; + } + + @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() { + try { + mCallback.onInlineSuggestionsSessionInvalidated(); + } catch (RemoteException e) { + Log.w(TAG, "onInlineSuggestionsSessionInvalidated() remote exception:" + e); + } + if (mResponseCallback != null) { + consumeInlineSuggestionsResponse(EMPTY_RESPONSE); + mResponseCallback.invalidate(); + mResponseCallback = null; + } + } + + /** + * 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. + */ + @MainThread + void makeInlineSuggestionRequestUncheck() { + if (mCallbackInvoked) { + return; + } + try { + final InlineSuggestionsRequest request = mRequestSupplier.apply( + mRequestInfo.getUiExtras()); + if (request == null) { + if (DEBUG) { + Log.d(TAG, "onCreateInlineSuggestionsRequest() returned null request"); + } + mCallback.onInlineSuggestionsUnsupported(); + } else { + request.setHostInputToken(mHostInputTokenSupplier.get()); + request.filterContentTypes(); + mResponseCallback = new InlineSuggestionsResponseCallbackImpl(this); + mCallback.onInlineSuggestionsRequest(request, mResponseCallback); + } + } catch (RemoteException e) { + Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e); + } + mCallbackInvoked = true; + } + + @MainThread + void handleOnInlineSuggestionsResponse(@NonNull AutofillId fieldId, + @NonNull InlineSuggestionsResponse response) { + if (!mInlineSuggestionSessionController.match(fieldId)) { + return; + } + 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); + } + + /** + * Internal implementation of {@link IInlineSuggestionsResponseCallback}. + */ + private static final class InlineSuggestionsResponseCallbackImpl extends + IInlineSuggestionsResponseCallback.Stub { + private final WeakReference<InlineSuggestionSession> mSession; + private volatile boolean mInvalid = false; + + private InlineSuggestionsResponseCallbackImpl(InlineSuggestionSession session) { + mSession = new WeakReference<>(session); + } + + void invalidate() { + mInvalid = true; + } + + @BinderThread + @Override + public void onInlineSuggestionsResponse(AutofillId fieldId, + InlineSuggestionsResponse response) { + if (mInvalid) { + return; + } + final InlineSuggestionSession session = mSession.get(); + if (session != null) { + session.mMainThreadHandler.sendMessage( + obtainMessage(InlineSuggestionSession::handleOnInlineSuggestionsResponse, + session, fieldId, response)); + } + } + } +} diff --git a/core/java/android/inputmethodservice/InlineSuggestionSessionController.java b/core/java/android/inputmethodservice/InlineSuggestionSessionController.java new file mode 100644 index 000000000000..071c096ee2f0 --- /dev/null +++ b/core/java/android/inputmethodservice/InlineSuggestionSessionController.java @@ -0,0 +1,262 @@ +/* + * 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) { + mSession.consumeInlineSuggestionsResponse(InlineSuggestionSession.EMPTY_RESPONSE); + // 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(); + } +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 44825a8f1502..c5a11abe1136 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -16,9 +16,11 @@ package android.inputmethodservice; -import static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE; +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -34,6 +36,7 @@ import android.app.ActivityManager; import android.app.Dialog; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; @@ -41,6 +44,7 @@ import android.database.ContentObserver; import android.graphics.Rect; import android.graphics.Region; import android.net.Uri; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -62,8 +66,11 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsets.Side; import android.view.WindowManager; import android.view.animation.AnimationUtils; import android.view.inputmethod.CompletionInfo; @@ -71,6 +78,8 @@ import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InlineSuggestionsRequest; +import android.view.inputmethod.InlineSuggestionsResponse; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; @@ -81,17 +90,21 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; +import android.window.WindowMetricsHelper; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry; +import com.android.internal.view.IInlineSuggestionsRequestCallback; +import com.android.internal.view.InlineSuggestionsRequestInfo; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; /** * InputMethodService provides a standard implementation of an InputMethod, @@ -431,7 +444,37 @@ public class InputMethodService extends AbstractInputMethodService { final Insets mTmpInsets = new Insets(); final int[] mTmpLocation = new int[2]; + private InlineSuggestionSessionController mInlineSuggestionSessionController; + + private boolean mAutomotiveHideNavBarForKeyboard; + private boolean mIsAutomotive; + + /** + * An opaque {@link Binder} token of window requesting {@link InputMethodImpl#showSoftInput} + * The original app window token is passed from client app window. + * {@link com.android.server.inputmethod.InputMethodManagerService} creates a unique dummy + * token to identify this window. + * This dummy token is only valid for a single call to {@link InputMethodImpl#showSoftInput}, + * after which it is set null until next call. + */ + private IBinder mCurShowInputToken; + + /** + * An opaque {@link Binder} token of window requesting {@link InputMethodImpl#hideSoftInput} + * The original app window token is passed from client app window. + * {@link com.android.server.inputmethod.InputMethodManagerService} creates a unique dummy + * token to identify this window. + * This dummy token is only valid for a single call to {@link InputMethodImpl#hideSoftInput}, + * after which it is set {@code null} until next call. + */ + private IBinder mCurHideInputToken; + final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> { + onComputeInsets(mTmpInsets); + if (!mViewsCreated) { + // The IME views are not ready, keep visible insets untouched. + mTmpInsets.visibleTopInsets = 0; + } if (isExtractViewShown()) { // In true fullscreen mode, we just say the window isn't covering // any content so we don't impact whatever is behind. @@ -440,12 +483,15 @@ public class InputMethodService extends AbstractInputMethodService { info.touchableRegion.setEmpty(); info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); } else { - onComputeInsets(mTmpInsets); info.contentInsets.top = mTmpInsets.contentTopInsets; info.visibleInsets.top = mTmpInsets.visibleTopInsets; info.touchableRegion.set(mTmpInsets.touchableRegion); info.setTouchableInsets(mTmpInsets.touchableInsets); } + + if (mInputFrame != null) { + setImeExclusionRect(mTmpInsets.visibleTopInsets); + } }; final View.OnClickListener mActionClickListener = v -> { @@ -466,6 +512,10 @@ public class InputMethodService extends AbstractInputMethodService { * all of the standard behavior for an input method. */ public class InputMethodImpl extends AbstractInputMethodImpl { + + private boolean mSystemCallingShowSoftInput; + private boolean mSystemCallingHideSoftInput; + /** * {@inheritDoc} * @hide @@ -486,6 +536,21 @@ public class InputMethodService extends AbstractInputMethodService { /** * {@inheritDoc} + * @hide + */ + @MainThread + @Override + public void onCreateInlineSuggestionsRequest( + @NonNull InlineSuggestionsRequestInfo requestInfo, + @NonNull IInlineSuggestionsRequestCallback cb) { + if (DEBUG) { + Log.d(TAG, "InputMethodService received onCreateInlineSuggestionsRequest()"); + } + mInlineSuggestionSessionController.onMakeInlineSuggestionsRequest(requestInfo, cb); + } + + /** + * {@inheritDoc} */ @MainThread @Override @@ -506,12 +571,10 @@ public class InputMethodService extends AbstractInputMethodService { @Override public void updateInputMethodDisplay(int displayId) { // Update display for adding IME window to the right display. - if (displayId != DEFAULT_DISPLAY) { - // TODO(b/111364446) Need to address context lifecycle issue if need to re-create - // for update resources & configuration correctly when show soft input - // in non-default display. - updateDisplay(displayId); - } + // TODO(b/111364446) Need to address context lifecycle issue if need to re-create + // for update resources & configuration correctly when show soft input + // in non-default display. + updateDisplay(displayId); } /** @@ -541,6 +604,7 @@ public class InputMethodService extends AbstractInputMethodService { public void unbindInput() { if (DEBUG) Log.v(TAG, "unbindInput(): binding=" + mInputBinding + " ic=" + mInputConnection); + // Unbind input is per process per display. onUnbindInput(); mInputBinding = null; mInputConnection = null; @@ -588,19 +652,40 @@ public class InputMethodService extends AbstractInputMethodService { /** * {@inheritDoc} + * @hide + */ + @MainThread + @Override + public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver, + IBinder hideInputToken) { + mSystemCallingHideSoftInput = true; + mCurHideInputToken = hideInputToken; + hideSoftInput(flags, resultReceiver); + mCurHideInputToken = null; + mSystemCallingHideSoftInput = false; + } + + /** + * {@inheritDoc} */ @MainThread @Override public void hideSoftInput(int flags, ResultReceiver resultReceiver) { if (DEBUG) Log.v(TAG, "hideSoftInput()"); + if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R + && !mSystemCallingHideSoftInput) { + Log.e(TAG, "IME shouldn't call hideSoftInput on itself." + + " Use requestHideSelf(int) itself"); + return; + } final boolean wasVisible = mIsPreRendered ? mDecorViewVisible && mWindowVisible : isInputViewShown(); + applyVisibilityInInsetsConsumerIfNecessary(false /* setVisible */); if (mIsPreRendered) { if (DEBUG) { Log.v(TAG, "Making IME window invisible"); } setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition); - applyVisibilityInInsetsConsumer(false /* setVisible */); onPreRenderedWindowVisibilityChanged(false /* setVisible */); } else { mShowInputFlags = 0; @@ -620,11 +705,33 @@ public class InputMethodService extends AbstractInputMethodService { /** * {@inheritDoc} + * @hide + */ + @MainThread + @Override + public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver, + IBinder showInputToken) { + mSystemCallingShowSoftInput = true; + mCurShowInputToken = showInputToken; + showSoftInput(flags, resultReceiver); + mCurShowInputToken = null; + mSystemCallingShowSoftInput = false; + } + + /** + * {@inheritDoc} */ @MainThread @Override public void showSoftInput(int flags, ResultReceiver resultReceiver) { if (DEBUG) Log.v(TAG, "showSoftInput()"); + // TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods. + if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R + && !mSystemCallingShowSoftInput) { + Log.e(TAG," IME shouldn't call showSoftInput on itself." + + " Use requestShowSelf(int) itself"); + return; + } final boolean wasVisible = mIsPreRendered ? mDecorViewVisible && mWindowVisible : isInputViewShown(); if (dispatchOnShowInputRequested(flags, false)) { @@ -632,11 +739,11 @@ public class InputMethodService extends AbstractInputMethodService { if (DEBUG) { Log.v(TAG, "Making IME window visible"); } - applyVisibilityInInsetsConsumer(true /* setVisible */); onPreRenderedWindowVisibilityChanged(true /* setVisible */); } else { showWindow(true); } + applyVisibilityInInsetsConsumerIfNecessary(true /* setVisible */); } // If user uses hard keyboard, IME button should always be shown. setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition); @@ -659,17 +766,96 @@ public class InputMethodService extends AbstractInputMethodService { public void changeInputMethodSubtype(InputMethodSubtype subtype) { dispatchOnCurrentInputMethodSubtypeChanged(subtype); } + + /** + * {@inheritDoc} + * @hide + */ + @Override + public void setCurrentShowInputToken(IBinder showInputToken) { + mCurShowInputToken = showInputToken; + } + + /** + * {@inheritDoc} + * @hide + */ + @Override + public void setCurrentHideInputToken(IBinder hideInputToken) { + mCurHideInputToken = hideInputToken; + } + } + + /** + * Called when Autofill is requesting an {@link InlineSuggestionsRequest} from the IME. + * + * <p>The Autofill Framework will first request the IME to create and send an + * {@link InlineSuggestionsRequest} back. Once Autofill Framework receives a valid request and + * also receives valid inline suggestions, they will be returned via + * {@link #onInlineSuggestionsResponse(InlineSuggestionsResponse)}.</p> + * + * <p>IME Lifecycle - The request will wait to be created after inputStarted</p> + * + * <p>If the IME wants to support displaying inline suggestions, they must set + * supportsInlineSuggestions in its XML and implement this method to return a valid + * {@link InlineSuggestionsRequest}.</p> + * + * @param uiExtras the extras that contain the UI renderer related information + * @return an {@link InlineSuggestionsRequest} to be sent to Autofill. + */ + @Nullable + public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(@NonNull Bundle uiExtras) { + return null; + } + + /** + * Called when Autofill responds back with {@link InlineSuggestionsResponse} containing + * inline suggestions. + * + * <p>Should be implemented by subclasses.</p> + * + * @param response {@link InlineSuggestionsResponse} passed back by Autofill. + * @return Whether the IME will use and render the inline suggestions. + */ + public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { + return false; + } + + /** + * Returns the {@link IBinder} input token from the host view root. + */ + @Nullable + private IBinder getHostInputToken() { + ViewRootImpl viewRoot = null; + if (mRootView != null) { + viewRoot = mRootView.getViewRootImpl(); + } + return viewRoot == null ? null : viewRoot.getInputToken(); } private void notifyImeHidden() { - setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition); - onPreRenderedWindowVisibilityChanged(false /* setVisible */); + requestHideSelf(0); + } + + private void removeImeSurface() { + if (!mShowInputRequested && !mWindowVisible) { + // hiding a window removes its surface. + mWindow.hide(); + } } private void setImeWindowStatus(int visibilityFlags, int backDisposition) { mPrivOps.setImeWindowStatus(visibilityFlags, backDisposition); } + /** Set region of the keyboard to be avoided from back gesture */ + private void setImeExclusionRect(int visibleTopInsets) { + View inputFrameRootView = mInputFrame.getRootView(); + Rect r = new Rect(0, visibleTopInsets, inputFrameRootView.getWidth(), + inputFrameRootView.getHeight()); + inputFrameRootView.setSystemGestureExclusionRects(Collections.singletonList(r)); + } + /** * Concrete implementation of * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides @@ -776,6 +962,14 @@ public class InputMethodService extends AbstractInputMethodService { public final void notifyImeHidden() { InputMethodService.this.notifyImeHidden(); } + + /** + * Notify IME that surface can be now removed. + * @hide + */ + public final void removeImeSurface() { + InputMethodService.this.removeImeSurface(); + } } /** @@ -997,6 +1191,11 @@ public class InputMethodService extends AbstractInputMethodService { super.onCreate(); mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); mSettingsObserver = SettingsObserver.createAndRegister(this); + + mIsAutomotive = isAutomotive(); + mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean( + com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard); + // TODO(b/111364446) Need to address context lifecycle issue if need to re-create // for update resources & configuration correctly when show soft input // in non-default display. @@ -1004,6 +1203,22 @@ public class InputMethodService extends AbstractInputMethodService { Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState, WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false); + mWindow.getWindow().getAttributes().setFitInsetsTypes(statusBars() | navigationBars()); + mWindow.getWindow().getAttributes().setFitInsetsSides(Side.all() & ~Side.BOTTOM); + mWindow.getWindow().getAttributes().setFitInsetsIgnoringVisibility(true); + + // IME layout should always be inset by navigation bar, no matter its current visibility, + // unless automotive requests it, since automotive may hide the navigation bar. + mWindow.getWindow().getDecorView().setOnApplyWindowInsetsListener( + (v, insets) -> v.onApplyWindowInsets( + new WindowInsets.Builder(insets).setInsets( + navigationBars(), + mIsAutomotive && mAutomotiveHideNavBarForKeyboard + ? android.graphics.Insets.NONE + : insets.getInsetsIgnoringVisibility(navigationBars()) + ) + .build())); + // For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set // by default (but IME developers can opt this out later if they want a new behavior). mWindow.getWindow().setFlags( @@ -1011,6 +1226,10 @@ public class InputMethodService extends AbstractInputMethodService { initViews(); mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); + + mInlineSuggestionSessionController = new InlineSuggestionSessionController( + this::onCreateInlineSuggestionsRequest, this::getHostInputToken, + this::onInlineSuggestionsResponse); } /** @@ -1220,8 +1439,9 @@ public class InputMethodService extends AbstractInputMethodService { * screen orientation changes. */ public int getMaxWidth() { - WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - return wm.getDefaultDisplay().getWidth(); + final WindowManager windowManager = getSystemService(WindowManager.class); + return WindowMetricsHelper.getBoundsExcludingNavigationBarAndCutout( + windowManager.getCurrentWindowMetrics()).width(); } /** @@ -1853,6 +2073,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 { @@ -1952,6 +2173,7 @@ public class InputMethodService extends AbstractInputMethodService { if (!mInputViewStarted) { if (DEBUG) Log.v(TAG, "CALL: onStartInputView"); mInputViewStarted = true; + mInlineSuggestionSessionController.notifyOnStartInputView(); onStartInputView(mInputEditorInfo, false); } } else if (!mCandidatesViewStarted) { @@ -1974,19 +2196,25 @@ public class InputMethodService extends AbstractInputMethodService { /** * Apply the IME visibility in {@link android.view.ImeInsetsSourceConsumer} when - * pre-rendering is enabled. + * {@link ViewRootImpl.sNewInsetsMode} is enabled. * @param setVisible {@code true} to make it visible, false to hide it. */ - private void applyVisibilityInInsetsConsumer(boolean setVisible) { - if (!mIsPreRendered) { + private void applyVisibilityInInsetsConsumerIfNecessary(boolean setVisible) { + if (!isVisibilityAppliedUsingInsetsConsumer()) { return; } - mPrivOps.applyImeVisibility(setVisible); + mPrivOps.applyImeVisibility(setVisible + ? mCurShowInputToken : mCurHideInputToken, setVisible); + } + + private boolean isVisibilityAppliedUsingInsetsConsumer() { + return ViewRootImpl.sNewInsetsMode > NEW_INSETS_MODE_NONE; } private void finishViews(boolean finishingInput) { if (mInputViewStarted) { if (DEBUG) Log.v(TAG, "CALL: onFinishInputView"); + mInlineSuggestionSessionController.notifyOnFinishInputView(); onFinishInputView(finishingInput); } else if (mCandidatesViewStarted) { if (DEBUG) Log.v(TAG, "CALL: onFinishCandidatesView"); @@ -2007,7 +2235,15 @@ public class InputMethodService extends AbstractInputMethodService { mWindowVisible = false; finishViews(false /* finishingInput */); if (mDecorViewVisible) { - mWindow.hide(); + // When insets API is enabled, it is responsible for client and server side + // visibility of IME window. + if (isVisibilityAppliedUsingInsetsConsumer()) { + if (mInputView != null) { + mInputView.dispatchWindowVisibilityChanged(View.GONE); + } + } else { + mWindow.hide(); + } mDecorViewVisible = false; onWindowHidden(); mDecorViewWasVisible = false; @@ -2073,6 +2309,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(); } @@ -2089,12 +2326,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; + mInlineSuggestionSessionController.notifyOnStartInputView(); onStartInputView(mInputEditorInfo, restarting); startExtractingText(true); } else if (mCandidatesVisibility == View.VISIBLE) { @@ -3032,6 +3273,11 @@ public class InputMethodService extends AbstractInputMethodService { : IME_VISIBLE) : 0); } + private boolean isAutomotive() { + return getApplicationContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE); + } + /** * Performs a dump of the InputMethodService's internal state. Override * to add your own information to the dump. diff --git a/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java b/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java index 31c948a14698..dbb669be1402 100644 --- a/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java +++ b/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java @@ -39,6 +39,7 @@ import android.view.inputmethod.ExtractedText; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IMultiClientInputMethodSession; +import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.os.SomeArgs; import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.view.IInputContext; @@ -46,7 +47,6 @@ import com.android.internal.view.IInputMethodSession; import com.android.internal.view.InputConnectionWrapper; import java.lang.ref.WeakReference; -import java.util.concurrent.atomic.AtomicBoolean; /** * Re-dispatches all the incoming per-client events to the specified {@link Looper} thread. @@ -80,19 +80,19 @@ final class MultiClientInputMethodClientCallbackAdaptor { @Nullable InputEventReceiver mInputEventReceiver; - private final AtomicBoolean mFinished = new AtomicBoolean(false); + private final CancellationGroup mCancellationGroup = new CancellationGroup(); IInputMethodSession.Stub createIInputMethodSession() { synchronized (mSessionLock) { return new InputMethodSessionImpl( - mSessionLock, mCallbackImpl, mHandler, mFinished); + mSessionLock, mCallbackImpl, mHandler, mCancellationGroup); } } IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() { synchronized (mSessionLock) { return new MultiClientInputMethodSessionImpl( - mSessionLock, mCallbackImpl, mHandler, mFinished); + mSessionLock, mCallbackImpl, mHandler, mCancellationGroup); } } @@ -105,7 +105,7 @@ final class MultiClientInputMethodClientCallbackAdaptor { mHandler = new Handler(looper, null, true); mReadChannel = readChannel; mInputEventReceiver = new ImeInputEventReceiver(mReadChannel, mHandler.getLooper(), - mFinished, mDispatcherState, mCallbackImpl.mOriginalCallback); + mCancellationGroup, mDispatcherState, mCallbackImpl.mOriginalCallback); } } @@ -139,16 +139,17 @@ final class MultiClientInputMethodClientCallbackAdaptor { } private static final class ImeInputEventReceiver extends InputEventReceiver { - private final AtomicBoolean mFinished; + private final CancellationGroup mCancellationGroupOnFinishSession; private final KeyEvent.DispatcherState mDispatcherState; private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback; private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor; - ImeInputEventReceiver(InputChannel readChannel, Looper looper, AtomicBoolean finished, + ImeInputEventReceiver(InputChannel readChannel, Looper looper, + CancellationGroup cancellationGroupOnFinishSession, KeyEvent.DispatcherState dispatcherState, MultiClientInputMethodServiceDelegate.ClientCallback callback) { super(readChannel, looper); - mFinished = finished; + mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession; mDispatcherState = dispatcherState; mClientCallback = callback; mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback); @@ -156,7 +157,7 @@ final class MultiClientInputMethodClientCallbackAdaptor { @Override public void onInputEvent(InputEvent event) { - if (mFinished.get()) { + if (mCancellationGroupOnFinishSession.isCanceled()) { // The session has been finished. finishInputEvent(event, false); return; @@ -187,14 +188,14 @@ final class MultiClientInputMethodClientCallbackAdaptor { private CallbackImpl mCallbackImpl; @GuardedBy("mSessionLock") private Handler mHandler; - private final AtomicBoolean mSessionFinished; + private final CancellationGroup mCancellationGroupOnFinishSession; InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler, - AtomicBoolean sessionFinished) { + CancellationGroup cancellationGroupOnFinishSession) { mSessionLock = lock; mCallbackImpl = callback; mHandler = handler; - mSessionFinished = sessionFinished; + mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession; } @Override @@ -272,7 +273,7 @@ final class MultiClientInputMethodClientCallbackAdaptor { if (mCallbackImpl == null || mHandler == null) { return; } - mSessionFinished.set(true); + mCancellationGroupOnFinishSession.cancelAll(); mHandler.sendMessage(PooledLambda.obtainMessage( CallbackImpl::finishSession, mCallbackImpl)); mCallbackImpl = null; @@ -296,6 +297,12 @@ final class MultiClientInputMethodClientCallbackAdaptor { // no-op for multi-session since IME is responsible controlling navigation bar buttons. reportNotSupported(); } + + @Override + public void removeImeSurface() { + // no-op for multi-session + reportNotSupported(); + } } private static final class MultiClientInputMethodSessionImpl @@ -305,14 +312,14 @@ final class MultiClientInputMethodClientCallbackAdaptor { private CallbackImpl mCallbackImpl; @GuardedBy("mSessionLock") private Handler mHandler; - private final AtomicBoolean mSessionFinished; + private final CancellationGroup mCancellationGroupOnFinishSession; MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback, - Handler handler, AtomicBoolean sessionFinished) { + Handler handler, CancellationGroup cancellationGroupOnFinishSession) { mSessionLock = lock; mCallbackImpl = callback; mHandler = handler; - mSessionFinished = sessionFinished; + mCancellationGroupOnFinishSession = cancellationGroupOnFinishSession; } @Override @@ -329,7 +336,7 @@ final class MultiClientInputMethodClientCallbackAdaptor { new WeakReference<>(null); args.arg1 = (inputContext == null) ? null : new InputConnectionWrapper(fakeIMS, inputContext, missingMethods, - mSessionFinished); + mCancellationGroupOnFinishSession); args.arg2 = editorInfo; args.argi1 = controlFlags; args.argi2 = softInputMode; diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index 356b3448430a..6efd03c44b9f 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.app.Dialog; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.Rect; import android.os.Debug; import android.os.IBinder; @@ -29,6 +28,7 @@ import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; import android.view.WindowManager; import java.lang.annotation.Retention; @@ -51,7 +51,6 @@ public class SoftInputWindow extends Dialog { final int mWindowType; final int mGravity; final boolean mTakesFocus; - final boolean mAutomotiveHideNavBarForKeyboard; private final Rect mBounds = new Rect(); @Retention(SOURCE) @@ -96,6 +95,13 @@ public class SoftInputWindow extends Dialog { lp.token = token; getWindow().setAttributes(lp); updateWindowState(SoftInputWindowState.TOKEN_SET); + + // As soon as we have a token, make sure the window is added (but not shown) by + // setting visibility to INVISIBLE and calling show() on Dialog. Note that + // WindowInsetsController.OnControllableInsetsChangedListener relies on the window + // being added to function. + getWindow().getDecorView().setVisibility(View.INVISIBLE); + show(); return; case SoftInputWindowState.TOKEN_SET: case SoftInputWindowState.SHOWN_AT_LEAST_ONCE: @@ -136,8 +142,6 @@ public class SoftInputWindow extends Dialog { mWindowType = windowType; mGravity = gravity; mTakesFocus = takesFocus; - mAutomotiveHideNavBarForKeyboard = context.getResources().getBoolean( - com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard); initDockWindow(); } @@ -251,11 +255,6 @@ public class SoftInputWindow extends Dialog { windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; } - if (isAutomotive() && mAutomotiveHideNavBarForKeyboard) { - windowSetFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; - windowModFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; - } - getWindow().setFlags(windowSetFlags, windowModFlags); } @@ -347,10 +346,6 @@ public class SoftInputWindow extends Dialog { mWindowState = newState; } - private boolean isAutomotive() { - return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); - } - private static String stateToString(@SoftInputWindowState int state) { switch (state) { case SoftInputWindowState.TOKEN_PENDING: |
