1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.inputmethod; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.os.IBinder; 22 import android.os.RemoteException; 23 import android.util.Slog; 24 import android.view.autofill.AutofillId; 25 import android.view.inputmethod.InlineSuggestionsRequest; 26 import android.view.inputmethod.InputMethodInfo; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback; 30 import com.android.internal.inputmethod.IInlineSuggestionsResponseCallback; 31 import com.android.internal.inputmethod.InlineSuggestionsRequestCallback; 32 import com.android.internal.inputmethod.InlineSuggestionsRequestInfo; 33 34 /** 35 * A controller managing autofill suggestion requests. 36 */ 37 final class AutofillSuggestionsController { 38 private static final boolean DEBUG = false; 39 private static final String TAG = AutofillSuggestionsController.class.getSimpleName(); 40 41 @NonNull private final InputMethodBindingController mBindingController; 42 43 /** 44 * The host input token of the input method that is currently associated with this controller. 45 */ 46 @GuardedBy("ImfLock.class") 47 @Nullable 48 private IBinder mCurHostInputToken; 49 50 private static final class CreateInlineSuggestionsRequest { 51 @NonNull final InlineSuggestionsRequestInfo mRequestInfo; 52 @NonNull final InlineSuggestionsRequestCallback mCallback; 53 @NonNull final String mPackageName; 54 CreateInlineSuggestionsRequest( @onNull InlineSuggestionsRequestInfo requestInfo, @NonNull InlineSuggestionsRequestCallback callback, @NonNull String packageName)55 CreateInlineSuggestionsRequest( 56 @NonNull InlineSuggestionsRequestInfo requestInfo, 57 @NonNull InlineSuggestionsRequestCallback callback, 58 @NonNull String packageName) { 59 mRequestInfo = requestInfo; 60 mCallback = callback; 61 mPackageName = packageName; 62 } 63 } 64 65 /** 66 * If a request to create inline autofill suggestions comes in while the IME is unbound 67 * due to {@link InputMethodManagerService#mPreventImeStartupUnlessTextEditor}, 68 * this is where it is stored, so that it may be fulfilled once the IME rebinds. 69 */ 70 @GuardedBy("ImfLock.class") 71 @Nullable 72 private CreateInlineSuggestionsRequest mPendingInlineSuggestionsRequest; 73 74 /** 75 * A callback into the autofill service obtained from the latest call to 76 * {@link #onCreateInlineSuggestionsRequest}, which can be used to invalidate an 77 * autofill session in case the IME process dies. 78 */ 79 @GuardedBy("ImfLock.class") 80 @Nullable 81 private InlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback; 82 AutofillSuggestionsController(@onNull InputMethodBindingController bindingController)83 AutofillSuggestionsController(@NonNull InputMethodBindingController bindingController) { 84 mBindingController = bindingController; 85 } 86 87 @GuardedBy("ImfLock.class") onResetSystemUi()88 void onResetSystemUi() { 89 mCurHostInputToken = null; 90 } 91 92 @Nullable 93 @GuardedBy("ImfLock.class") getCurHostInputToken()94 IBinder getCurHostInputToken() { 95 return mCurHostInputToken; 96 } 97 98 @GuardedBy("ImfLock.class") onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback callback, boolean touchExplorationEnabled)99 void onCreateInlineSuggestionsRequest(InlineSuggestionsRequestInfo requestInfo, 100 InlineSuggestionsRequestCallback callback, boolean touchExplorationEnabled) { 101 clearPendingInlineSuggestionsRequest(); 102 mInlineSuggestionsRequestCallback = callback; 103 104 // Note that current user ID is guaranteed to be userId. 105 final var imeId = mBindingController.getSelectedMethodId(); 106 final InputMethodInfo imi = InputMethodSettingsRepository.get(mBindingController.mUserId) 107 .getMethodMap().get(imeId); 108 if (imi == null || !isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) { 109 callback.onInlineSuggestionsUnsupported(); 110 return; 111 } 112 113 mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest( 114 requestInfo, callback, imi.getPackageName()); 115 if (mBindingController.getCurMethod() != null) { 116 // In the normal case when the IME is connected, we can make the request here. 117 performOnCreateInlineSuggestionsRequest(); 118 } else { 119 // Otherwise, the next time the IME connection is established, 120 // InputMethodBindingController.mMainConnection#onServiceConnected() will call 121 // into #performOnCreateInlineSuggestionsRequestLocked() to make the request. 122 if (DEBUG) { 123 Slog.d(TAG, "IME not connected. Delaying inline suggestions request."); 124 } 125 } 126 } 127 128 @GuardedBy("ImfLock.class") performOnCreateInlineSuggestionsRequest()129 void performOnCreateInlineSuggestionsRequest() { 130 if (mPendingInlineSuggestionsRequest == null) { 131 return; 132 } 133 IInputMethodInvoker curMethod = mBindingController.getCurMethod(); 134 if (DEBUG) { 135 Slog.d(TAG, "Performing onCreateInlineSuggestionsRequest. mCurMethod = " + curMethod); 136 } 137 if (curMethod != null) { 138 final IInlineSuggestionsRequestCallback callback = 139 new InlineSuggestionsRequestCallbackDecorator( 140 mPendingInlineSuggestionsRequest.mCallback, 141 mPendingInlineSuggestionsRequest.mPackageName, 142 mBindingController.getCurTokenDisplayId(), 143 mBindingController.getCurToken()); 144 curMethod.onCreateInlineSuggestionsRequest( 145 mPendingInlineSuggestionsRequest.mRequestInfo, callback); 146 } else { 147 Slog.w(TAG, "No IME connected! Abandoning inline suggestions creation request."); 148 } 149 clearPendingInlineSuggestionsRequest(); 150 } 151 152 @GuardedBy("ImfLock.class") clearPendingInlineSuggestionsRequest()153 private void clearPendingInlineSuggestionsRequest() { 154 mPendingInlineSuggestionsRequest = null; 155 } 156 isInlineSuggestionsEnabled(InputMethodInfo imi, boolean touchExplorationEnabled)157 private static boolean isInlineSuggestionsEnabled(InputMethodInfo imi, 158 boolean touchExplorationEnabled) { 159 return imi.isInlineSuggestionsEnabled() 160 && (!touchExplorationEnabled 161 || imi.supportsInlineSuggestionsWithTouchExploration()); 162 } 163 164 @GuardedBy("ImfLock.class") invalidateAutofillSession()165 void invalidateAutofillSession() { 166 if (mInlineSuggestionsRequestCallback != null) { 167 mInlineSuggestionsRequestCallback.onInlineSuggestionsSessionInvalidated(); 168 } 169 } 170 171 /** 172 * The decorator which validates the host package name in the 173 * {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name. 174 */ 175 private final class InlineSuggestionsRequestCallbackDecorator 176 extends IInlineSuggestionsRequestCallback.Stub { 177 @NonNull private final InlineSuggestionsRequestCallback mCallback; 178 @NonNull private final String mImePackageName; 179 private final int mImeDisplayId; 180 @NonNull private final IBinder mImeToken; 181 InlineSuggestionsRequestCallbackDecorator( @onNull InlineSuggestionsRequestCallback callback, @NonNull String imePackageName, int displayId, @NonNull IBinder imeToken)182 InlineSuggestionsRequestCallbackDecorator( 183 @NonNull InlineSuggestionsRequestCallback callback, @NonNull String imePackageName, 184 int displayId, @NonNull IBinder imeToken) { 185 mCallback = callback; 186 mImePackageName = imePackageName; 187 mImeDisplayId = displayId; 188 mImeToken = imeToken; 189 } 190 191 @Override onInlineSuggestionsUnsupported()192 public void onInlineSuggestionsUnsupported() { 193 mCallback.onInlineSuggestionsUnsupported(); 194 } 195 196 @Override onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)197 public void onInlineSuggestionsRequest(InlineSuggestionsRequest request, 198 IInlineSuggestionsResponseCallback callback) 199 throws RemoteException { 200 if (!mImePackageName.equals(request.getHostPackageName())) { 201 throw new SecurityException( 202 "Host package name in the provide request=[" + request.getHostPackageName() 203 + "] doesn't match the IME package name=[" + mImePackageName 204 + "]."); 205 } 206 request.setHostDisplayId(mImeDisplayId); 207 synchronized (ImfLock.class) { 208 final IBinder curImeToken = mBindingController.getCurToken(); 209 if (mImeToken == curImeToken) { 210 mCurHostInputToken = request.getHostInputToken(); 211 } 212 } 213 mCallback.onInlineSuggestionsRequest(request, callback); 214 } 215 216 @Override onInputMethodStartInput(AutofillId imeFieldId)217 public void onInputMethodStartInput(AutofillId imeFieldId) { 218 mCallback.onInputMethodStartInput(imeFieldId); 219 } 220 221 @Override onInputMethodShowInputRequested(boolean requestResult)222 public void onInputMethodShowInputRequested(boolean requestResult) { 223 mCallback.onInputMethodShowInputRequested(requestResult); 224 } 225 226 @Override onInputMethodStartInputView()227 public void onInputMethodStartInputView() { 228 mCallback.onInputMethodStartInputView(); 229 } 230 231 @Override onInputMethodFinishInputView()232 public void onInputMethodFinishInputView() { 233 mCallback.onInputMethodFinishInputView(); 234 } 235 236 @Override onInputMethodFinishInput()237 public void onInputMethodFinishInput() { 238 mCallback.onInputMethodFinishInput(); 239 } 240 241 @Override onInlineSuggestionsSessionInvalidated()242 public void onInlineSuggestionsSessionInvalidated() { 243 mCallback.onInlineSuggestionsSessionInvalidated(); 244 } 245 } 246 } 247