blob: 035a7485fe86ed5898c0b3e5a442d72a0f99f9a4 [file] [log] [blame]
/*
* Copyright (C) 2022 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.inputmethod;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
import android.view.autofill.AutofillId;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InputMethodInfo;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
import com.android.internal.inputmethod.IInlineSuggestionsResponseCallback;
import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
/**
* A controller managing autofill suggestion requests.
*/
final class AutofillSuggestionsController {
private static final boolean DEBUG = false;
private static final String TAG = AutofillSuggestionsController.class.getSimpleName();
@NonNull private final InputMethodManagerService mService;
private static final class CreateInlineSuggestionsRequest {
@NonNull final InlineSuggestionsRequestInfo mRequestInfo;
@NonNull final IInlineSuggestionsRequestCallback mCallback;
@NonNull final String mPackageName;
CreateInlineSuggestionsRequest(
@NonNull InlineSuggestionsRequestInfo requestInfo,
@NonNull IInlineSuggestionsRequestCallback callback,
@NonNull String packageName) {
mRequestInfo = requestInfo;
mCallback = callback;
mPackageName = packageName;
}
}
/**
* If a request to create inline autofill suggestions comes in while the IME is unbound
* due to {@link InputMethodManagerService#mPreventImeStartupUnlessTextEditor},
* this is where it is stored, so that it may be fulfilled once the IME rebinds.
*/
@GuardedBy("ImfLock.class")
@Nullable
private CreateInlineSuggestionsRequest mPendingInlineSuggestionsRequest;
/**
* A callback into the autofill service obtained from the latest call to
* {@link #onCreateInlineSuggestionsRequest}, which can be used to invalidate an
* autofill session in case the IME process dies.
*/
@GuardedBy("ImfLock.class")
@Nullable
private IInlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback;
AutofillSuggestionsController(@NonNull InputMethodManagerService service) {
mService = service;
}
@GuardedBy("ImfLock.class")
void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
boolean touchExplorationEnabled) {
clearPendingInlineSuggestionsRequest();
mInlineSuggestionsRequestCallback = callback;
final InputMethodInfo imi = mService.queryInputMethodForCurrentUserLocked(
mService.getSelectedMethodIdLocked());
try {
if (userId == mService.getCurrentImeUserIdLocked()
&& imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
requestInfo, callback, imi.getPackageName());
if (mService.getCurMethodLocked() != null) {
// In the normal case when the IME is connected, we can make the request here.
performOnCreateInlineSuggestionsRequest();
} else {
// Otherwise, the next time the IME connection is established,
// InputMethodBindingController.mMainConnection#onServiceConnected() will call
// into #performOnCreateInlineSuggestionsRequestLocked() to make the request.
if (DEBUG) {
Slog.d(TAG, "IME not connected. Delaying inline suggestions request.");
}
}
} else {
callback.onInlineSuggestionsUnsupported();
}
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException calling onCreateInlineSuggestionsRequest(): " + e);
}
}
@GuardedBy("ImfLock.class")
void performOnCreateInlineSuggestionsRequest() {
if (mPendingInlineSuggestionsRequest == null) {
return;
}
IInputMethodInvoker curMethod = mService.getCurMethodLocked();
if (DEBUG) {
Slog.d(TAG, "Performing onCreateInlineSuggestionsRequest. mCurMethod = " + curMethod);
}
if (curMethod != null) {
final IInlineSuggestionsRequestCallback callback =
new InlineSuggestionsRequestCallbackDecorator(
mPendingInlineSuggestionsRequest.mCallback,
mPendingInlineSuggestionsRequest.mPackageName,
mService.getCurTokenDisplayIdLocked(),
mService.getCurTokenLocked(),
mService);
curMethod.onCreateInlineSuggestionsRequest(
mPendingInlineSuggestionsRequest.mRequestInfo, callback);
} else {
Slog.w(TAG, "No IME connected! Abandoning inline suggestions creation request.");
}
clearPendingInlineSuggestionsRequest();
}
@GuardedBy("ImfLock.class")
private void clearPendingInlineSuggestionsRequest() {
mPendingInlineSuggestionsRequest = null;
}
private static boolean isInlineSuggestionsEnabled(InputMethodInfo imi,
boolean touchExplorationEnabled) {
return imi.isInlineSuggestionsEnabled()
&& (!touchExplorationEnabled
|| imi.supportsInlineSuggestionsWithTouchExploration());
}
@GuardedBy("ImfLock.class")
void invalidateAutofillSession() {
if (mInlineSuggestionsRequestCallback != null) {
try {
mInlineSuggestionsRequestCallback.onInlineSuggestionsSessionInvalidated();
} catch (RemoteException e) {
Slog.e(TAG, "Cannot invalidate autofill session.", e);
}
}
}
/**
* The decorator which validates the host package name in the
* {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name.
*/
private static final class InlineSuggestionsRequestCallbackDecorator
extends IInlineSuggestionsRequestCallback.Stub {
@NonNull private final IInlineSuggestionsRequestCallback mCallback;
@NonNull private final String mImePackageName;
private final int mImeDisplayId;
@NonNull private final IBinder mImeToken;
@NonNull private final InputMethodManagerService mImms;
InlineSuggestionsRequestCallbackDecorator(
@NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName,
int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms) {
mCallback = callback;
mImePackageName = imePackageName;
mImeDisplayId = displayId;
mImeToken = imeToken;
mImms = imms;
}
@Override
public void onInlineSuggestionsUnsupported() throws RemoteException {
mCallback.onInlineSuggestionsUnsupported();
}
@Override
public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
IInlineSuggestionsResponseCallback callback)
throws RemoteException {
if (!mImePackageName.equals(request.getHostPackageName())) {
throw new SecurityException(
"Host package name in the provide request=[" + request.getHostPackageName()
+ "] doesn't match the IME package name=[" + mImePackageName
+ "].");
}
request.setHostDisplayId(mImeDisplayId);
mImms.setCurHostInputToken(mImeToken, request.getHostInputToken());
mCallback.onInlineSuggestionsRequest(request, callback);
}
@Override
public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
mCallback.onInputMethodStartInput(imeFieldId);
}
@Override
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();
}
@Override
public void onInlineSuggestionsSessionInvalidated() throws RemoteException {
mCallback.onInlineSuggestionsSessionInvalidated();
}
}
}