263 lines
11 KiB
Java
263 lines
11 KiB
Java
/*
|
|
* 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.inputmethod.IInlineSuggestionsRequestCallback;
|
|
import com.android.internal.inputmethod.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();
|
|
}
|
|
}
|