blob: 84fbe9a75a18f464c491ff3092b37b5292d11213 [file] [log] [blame]
/*
* 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 com.android.server.autofill;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sVerbose;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Slog;
import android.view.autofill.AutofillId;
import android.view.inputmethod.InlineSuggestion;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.view.IInlineSuggestionsRequestCallback;
import com.android.internal.view.IInlineSuggestionsResponseCallback;
import com.android.internal.view.InlineSuggestionsRequestInfo;
import com.android.server.autofill.ui.InlineFillUi;
import com.android.server.inputmethod.InputMethodManagerInternal;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Maintains an inline suggestion session with the IME.
*
* <p> Each session corresponds to one request from the Autofill manager service to create an
* {@link InlineSuggestionsRequest}. It's responsible for receiving callbacks from the IME and
* sending {@link android.view.inputmethod.InlineSuggestionsResponse} to IME.
*/
final class AutofillInlineSuggestionsRequestSession {
private static final String TAG = AutofillInlineSuggestionsRequestSession.class.getSimpleName();
@NonNull
private final InputMethodManagerInternal mInputMethodManagerInternal;
private final int mUserId;
@NonNull
private final ComponentName mComponentName;
@NonNull
private final Object mLock;
@NonNull
private final Handler mHandler;
@NonNull
private final Bundle mUiExtras;
@NonNull
private final InlineFillUi.InlineUiEventCallback mUiCallback;
@GuardedBy("mLock")
@NonNull
private AutofillId mAutofillId;
@GuardedBy("mLock")
@Nullable
private Consumer<InlineSuggestionsRequest> mImeRequestConsumer;
@GuardedBy("mLock")
private boolean mImeRequestReceived;
@GuardedBy("mLock")
@Nullable
private InlineSuggestionsRequest mImeRequest;
@GuardedBy("mLock")
@Nullable
private IInlineSuggestionsResponseCallback mResponseCallback;
@GuardedBy("mLock")
@Nullable
private AutofillId mImeCurrentFieldId;
@GuardedBy("mLock")
private boolean mImeInputStarted;
@GuardedBy("mLock")
private boolean mImeInputViewStarted;
@GuardedBy("mLock")
@Nullable
private InlineFillUi mInlineFillUi;
@GuardedBy("mLock")
private Boolean mPreviousResponseIsNotEmpty = null;
@GuardedBy("mLock")
private boolean mDestroyed = false;
@GuardedBy("mLock")
private boolean mPreviousHasNonPinSuggestionShow;
@GuardedBy("mLock")
private boolean mImeSessionInvalidated = false;
AutofillInlineSuggestionsRequestSession(
@NonNull InputMethodManagerInternal inputMethodManagerInternal, int userId,
@NonNull ComponentName componentName, @NonNull Handler handler, @NonNull Object lock,
@NonNull AutofillId autofillId,
@NonNull Consumer<InlineSuggestionsRequest> requestConsumer, @NonNull Bundle uiExtras,
@NonNull InlineFillUi.InlineUiEventCallback callback) {
mInputMethodManagerInternal = inputMethodManagerInternal;
mUserId = userId;
mComponentName = componentName;
mHandler = handler;
mLock = lock;
mUiExtras = uiExtras;
mUiCallback = callback;
mAutofillId = autofillId;
mImeRequestConsumer = requestConsumer;
}
@GuardedBy("mLock")
@NonNull
AutofillId getAutofillIdLocked() {
return mAutofillId;
}
/**
* Returns the {@link InlineSuggestionsRequest} provided by IME.
*
* <p> The caller is responsible for making sure Autofill hears back from IME before calling
* this method, using the {@link #mImeRequestConsumer}.
*/
@GuardedBy("mLock")
Optional<InlineSuggestionsRequest> getInlineSuggestionsRequestLocked() {
if (mDestroyed) {
return Optional.empty();
}
return Optional.ofNullable(mImeRequest);
}
/**
* Requests showing the inline suggestion in the IME when the IME becomes visible and is focused
* on the {@code autofillId}.
*
* @return false if the IME callback is not available.
*/
@GuardedBy("mLock")
boolean onInlineSuggestionsResponseLocked(@NonNull InlineFillUi inlineFillUi) {
if (mDestroyed) {
return false;
}
if (sDebug) {
Slog.d(TAG,
"onInlineSuggestionsResponseLocked called for:" + inlineFillUi.getAutofillId());
}
if (mImeRequest == null || mResponseCallback == null || mImeSessionInvalidated) {
return false;
}
// TODO(b/151123764): each session should only correspond to one field.
mAutofillId = inlineFillUi.getAutofillId();
mInlineFillUi = inlineFillUi;
maybeUpdateResponseToImeLocked();
return true;
}
/**
* Prevents further interaction with the IME. Must be called before starting a new request
* session to avoid unwanted behavior from two overlapping requests.
*/
@GuardedBy("mLock")
void destroySessionLocked() {
mDestroyed = true;
if (!mImeRequestReceived) {
Slog.w(TAG,
"Never received an InlineSuggestionsRequest from the IME for " + mAutofillId);
}
}
/**
* Requests the IME to create an {@link InlineSuggestionsRequest}.
*
* <p> This method should only be called once per session.
*/
@GuardedBy("mLock")
void onCreateInlineSuggestionsRequestLocked() {
if (mDestroyed) {
return;
}
mImeSessionInvalidated = false;
if (sDebug) Slog.d(TAG, "onCreateInlineSuggestionsRequestLocked called: " + mAutofillId);
mInputMethodManagerInternal.onCreateInlineSuggestionsRequest(mUserId,
new InlineSuggestionsRequestInfo(mComponentName, mAutofillId, mUiExtras),
new InlineSuggestionsRequestCallbackImpl(this));
}
/**
* Clear the locally cached inline fill UI, but don't clear the suggestion in IME.
*
* See also {@link AutofillInlineSessionController#resetInlineFillUiLocked()}
*/
@GuardedBy("mLock")
void resetInlineFillUiLocked() {
mInlineFillUi = null;
}
/**
* Optionally sends inline response to the IME, depending on the current state.
*/
@GuardedBy("mLock")
private void maybeUpdateResponseToImeLocked() {
if (sVerbose) Slog.v(TAG, "maybeUpdateResponseToImeLocked called");
if (mDestroyed || mResponseCallback == null) {
return;
}
if (mImeInputViewStarted && mInlineFillUi != null && match(mAutofillId,
mImeCurrentFieldId)) {
// if IME is visible, and response is not null, send the response
InlineSuggestionsResponse response = mInlineFillUi.getInlineSuggestionsResponse();
boolean isEmptyResponse = response.getInlineSuggestions().isEmpty();
if (isEmptyResponse && Boolean.FALSE.equals(mPreviousResponseIsNotEmpty)) {
// No-op if both the previous response and current response are empty.
return;
}
maybeNotifyFillUiEventLocked(response.getInlineSuggestions());
updateResponseToImeUncheckLocked(response);
mPreviousResponseIsNotEmpty = !isEmptyResponse;
}
}
/**
* Sends the {@code response} to the IME, assuming all the relevant checks are already done.
*/
@GuardedBy("mLock")
private void updateResponseToImeUncheckLocked(InlineSuggestionsResponse response) {
if (mDestroyed) {
return;
}
if (sDebug) Slog.d(TAG, "Send inline response: " + response.getInlineSuggestions().size());
try {
mResponseCallback.onInlineSuggestionsResponse(mAutofillId, response);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException sending InlineSuggestionsResponse to IME");
}
}
@GuardedBy("mLock")
private void maybeNotifyFillUiEventLocked(@NonNull List<InlineSuggestion> suggestions) {
if (mDestroyed) {
return;
}
boolean hasSuggestionToShow = false;
for (int i = 0; i < suggestions.size(); i++) {
InlineSuggestion suggestion = suggestions.get(i);
// It is possible we don't have any match result but we still have pinned
// suggestions. Only notify we have non-pinned suggestions to show
if (!suggestion.getInfo().isPinned()) {
hasSuggestionToShow = true;
break;
}
}
if (sDebug) {
Slog.d(TAG, "maybeNotifyFillUiEventLoked(): hasSuggestionToShow=" + hasSuggestionToShow
+ ", mPreviousHasNonPinSuggestionShow=" + mPreviousHasNonPinSuggestionShow);
}
// Use mPreviousHasNonPinSuggestionShow to save previous status, if the display status
// change, we can notify the event.
if (hasSuggestionToShow && !mPreviousHasNonPinSuggestionShow) {
// From no suggestion to has suggestions to show
mUiCallback.notifyInlineUiShown(mAutofillId);
} else if (!hasSuggestionToShow && mPreviousHasNonPinSuggestionShow) {
// From has suggestions to no suggestions to show
mUiCallback.notifyInlineUiHidden(mAutofillId);
}
// Update the latest status
mPreviousHasNonPinSuggestionShow = hasSuggestionToShow;
}
/**
* Handles the {@code request} and {@code callback} received from the IME.
*
* <p> Should only invoked in the {@link #mHandler} thread.
*/
private void handleOnReceiveImeRequest(@Nullable InlineSuggestionsRequest request,
@Nullable IInlineSuggestionsResponseCallback callback) {
synchronized (mLock) {
if (mDestroyed || mImeRequestReceived) {
return;
}
mImeRequestReceived = true;
mImeSessionInvalidated = false;
if (request != null && callback != null) {
mImeRequest = request;
mResponseCallback = callback;
handleOnReceiveImeStatusUpdated(mAutofillId, true, false);
}
if (mImeRequestConsumer != null) {
// Note that mImeRequest is only set if both request and callback are non-null.
mImeRequestConsumer.accept(mImeRequest);
mImeRequestConsumer = null;
}
}
}
/**
* Handles the IME status updates received from the IME.
*
* <p> Should only be invoked in the {@link #mHandler} thread.
*/
private void handleOnReceiveImeStatusUpdated(boolean imeInputStarted,
boolean imeInputViewStarted) {
synchronized (mLock) {
if (mDestroyed) {
return;
}
if (mImeCurrentFieldId != null) {
boolean imeInputStartedChanged = (mImeInputStarted != imeInputStarted);
boolean imeInputViewStartedChanged = (mImeInputViewStarted != imeInputViewStarted);
mImeInputStarted = imeInputStarted;
mImeInputViewStarted = imeInputViewStarted;
if (imeInputStartedChanged || imeInputViewStartedChanged) {
maybeUpdateResponseToImeLocked();
}
}
}
}
/**
* Handles the IME status updates received from the IME.
*
* <p> Should only be invoked in the {@link #mHandler} thread.
*/
private void handleOnReceiveImeStatusUpdated(@Nullable AutofillId imeFieldId,
boolean imeInputStarted, boolean imeInputViewStarted) {
synchronized (mLock) {
if (mDestroyed) {
return;
}
if (imeFieldId != null) {
mImeCurrentFieldId = imeFieldId;
}
handleOnReceiveImeStatusUpdated(imeInputStarted, imeInputViewStarted);
}
}
/**
* Handles the IME session status received from the IME.
*
* <p> Should only be invoked in the {@link #mHandler} thread.
*/
private void handleOnReceiveImeSessionInvalidated() {
synchronized (mLock) {
if (mDestroyed) {
return;
}
mImeSessionInvalidated = true;
}
}
/**
* Internal implementation of {@link IInlineSuggestionsRequestCallback}.
*/
private static final class InlineSuggestionsRequestCallbackImpl extends
IInlineSuggestionsRequestCallback.Stub {
private final WeakReference<AutofillInlineSuggestionsRequestSession> mSession;
private InlineSuggestionsRequestCallbackImpl(
AutofillInlineSuggestionsRequestSession session) {
mSession = new WeakReference<>(session);
}
@BinderThread
@Override
public void onInlineSuggestionsUnsupported() throws RemoteException {
if (sDebug) Slog.d(TAG, "onInlineSuggestionsUnsupported() called.");
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session,
null, null));
}
}
@BinderThread
@Override
public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
IInlineSuggestionsResponseCallback callback) {
if (sDebug) Slog.d(TAG, "onInlineSuggestionsRequest() received: " + request);
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session,
request, callback));
}
}
@Override
public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
if (sVerbose) Slog.v(TAG, "onInputMethodStartInput() received on " + imeFieldId);
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
session, imeFieldId, true, false));
}
}
@Override
public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
if (sVerbose) {
Slog.v(TAG, "onInputMethodShowInputRequested() received: " + requestResult);
}
}
@BinderThread
@Override
public void onInputMethodStartInputView() {
if (sVerbose) Slog.v(TAG, "onInputMethodStartInputView() received");
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
session, true, true));
}
}
@BinderThread
@Override
public void onInputMethodFinishInputView() {
if (sVerbose) Slog.v(TAG, "onInputMethodFinishInputView() received");
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
session, true, false));
}
}
@Override
public void onInputMethodFinishInput() throws RemoteException {
if (sVerbose) Slog.v(TAG, "onInputMethodFinishInput() received");
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated,
session, false, false));
}
}
@BinderThread
@Override
public void onInlineSuggestionsSessionInvalidated() throws RemoteException {
if (sDebug) Slog.d(TAG, "onInlineSuggestionsSessionInvalidated() called.");
final AutofillInlineSuggestionsRequestSession session = mSession.get();
if (session != null) {
session.mHandler.sendMessage(obtainMessage(
AutofillInlineSuggestionsRequestSession
::handleOnReceiveImeSessionInvalidated, session));
}
}
}
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();
}
}