| /* |
| * 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.ArrayMap; |
| 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 CarAutofillSuggestionsController implements AutofillController { |
| private static final boolean DEBUG = false; |
| private static final String TAG = CarAutofillSuggestionsController.class.getSimpleName(); |
| |
| @NonNull private final CarInputMethodManagerService mService; |
| @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap; |
| @NonNull private final InputMethodUtils.InputMethodSettings mSettings; |
| |
| 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 CarInputMethodManagerService#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; |
| |
| CarAutofillSuggestionsController(@NonNull CarInputMethodManagerService service) { |
| mService = service; |
| mMethodMap = mService.mMethodMap; |
| mSettings = mService.mSettings; |
| } |
| |
| @GuardedBy("ImfLock.class") |
| @Override |
| public void onCreateInlineSuggestionsRequest(@UserIdInt int userId, |
| InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback, |
| boolean touchExplorationEnabled) { |
| clearPendingInlineSuggestionsRequest(); |
| mInlineSuggestionsRequestCallback = callback; |
| final InputMethodInfo imi = mMethodMap.get(mService.getSelectedMethodIdLocked()); |
| try { |
| if (userId == mSettings.getCurrentUserId() |
| && 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") |
| public 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") |
| public 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 CarInputMethodManagerService mImms; |
| |
| InlineSuggestionsRequestCallbackDecorator( |
| @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName, |
| int displayId, @NonNull IBinder imeToken, |
| @NonNull CarInputMethodManagerService 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(); |
| } |
| } |
| } |