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.annotation.UserIdInt; 22 import android.os.IBinder; 23 import android.os.RemoteException; 24 import android.util.ArrayMap; 25 import android.util.Slog; 26 import android.view.autofill.AutofillId; 27 import android.view.inputmethod.InlineSuggestionsRequest; 28 import android.view.inputmethod.InputMethodInfo; 29 30 import com.android.internal.annotations.GuardedBy; 31 import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback; 32 import com.android.internal.inputmethod.IInlineSuggestionsResponseCallback; 33 import com.android.internal.inputmethod.InlineSuggestionsRequestInfo; 34 35 /** 36 * A controller managing autofill suggestion requests. 37 */ 38 final class AutofillSuggestionsController { 39 private static final boolean DEBUG = false; 40 private static final String TAG = AutofillSuggestionsController.class.getSimpleName(); 41 42 @NonNull private final InputMethodManagerService mService; 43 @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap; 44 @NonNull private final InputMethodUtils.InputMethodSettings mSettings; 45 46 private static final class CreateInlineSuggestionsRequest { 47 @NonNull final InlineSuggestionsRequestInfo mRequestInfo; 48 @NonNull final IInlineSuggestionsRequestCallback mCallback; 49 @NonNull final String mPackageName; 50 CreateInlineSuggestionsRequest( @onNull InlineSuggestionsRequestInfo requestInfo, @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String packageName)51 CreateInlineSuggestionsRequest( 52 @NonNull InlineSuggestionsRequestInfo requestInfo, 53 @NonNull IInlineSuggestionsRequestCallback callback, 54 @NonNull String packageName) { 55 mRequestInfo = requestInfo; 56 mCallback = callback; 57 mPackageName = packageName; 58 } 59 } 60 61 /** 62 * If a request to create inline autofill suggestions comes in while the IME is unbound 63 * due to {@link InputMethodManagerService#mPreventImeStartupUnlessTextEditor}, 64 * this is where it is stored, so that it may be fulfilled once the IME rebinds. 65 */ 66 @GuardedBy("ImfLock.class") 67 @Nullable 68 private CreateInlineSuggestionsRequest mPendingInlineSuggestionsRequest; 69 70 /** 71 * A callback into the autofill service obtained from the latest call to 72 * {@link #onCreateInlineSuggestionsRequest}, which can be used to invalidate an 73 * autofill session in case the IME process dies. 74 */ 75 @GuardedBy("ImfLock.class") 76 @Nullable 77 private IInlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback; 78 AutofillSuggestionsController(@onNull InputMethodManagerService service)79 AutofillSuggestionsController(@NonNull InputMethodManagerService service) { 80 mService = service; 81 mMethodMap = mService.mMethodMap; 82 mSettings = mService.mSettings; 83 } 84 85 @GuardedBy("ImfLock.class") onCreateInlineSuggestionsRequest(@serIdInt int userId, InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback, boolean touchExplorationEnabled)86 void onCreateInlineSuggestionsRequest(@UserIdInt int userId, 87 InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback, 88 boolean touchExplorationEnabled) { 89 clearPendingInlineSuggestionsRequest(); 90 mInlineSuggestionsRequestCallback = callback; 91 final InputMethodInfo imi = mMethodMap.get(mService.getSelectedMethodIdLocked()); 92 try { 93 if (userId == mSettings.getCurrentUserId() 94 && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) { 95 mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest( 96 requestInfo, callback, imi.getPackageName()); 97 if (mService.getCurMethodLocked() != null) { 98 // In the normal case when the IME is connected, we can make the request here. 99 performOnCreateInlineSuggestionsRequest(); 100 } else { 101 // Otherwise, the next time the IME connection is established, 102 // InputMethodBindingController.mMainConnection#onServiceConnected() will call 103 // into #performOnCreateInlineSuggestionsRequestLocked() to make the request. 104 if (DEBUG) { 105 Slog.d(TAG, "IME not connected. Delaying inline suggestions request."); 106 } 107 } 108 } else { 109 callback.onInlineSuggestionsUnsupported(); 110 } 111 } catch (RemoteException e) { 112 Slog.w(TAG, "RemoteException calling onCreateInlineSuggestionsRequest(): " + e); 113 } 114 } 115 116 @GuardedBy("ImfLock.class") performOnCreateInlineSuggestionsRequest()117 void performOnCreateInlineSuggestionsRequest() { 118 if (mPendingInlineSuggestionsRequest == null) { 119 return; 120 } 121 IInputMethodInvoker curMethod = mService.getCurMethodLocked(); 122 if (DEBUG) { 123 Slog.d(TAG, "Performing onCreateInlineSuggestionsRequest. mCurMethod = " + curMethod); 124 } 125 if (curMethod != null) { 126 final IInlineSuggestionsRequestCallback callback = 127 new InlineSuggestionsRequestCallbackDecorator( 128 mPendingInlineSuggestionsRequest.mCallback, 129 mPendingInlineSuggestionsRequest.mPackageName, 130 mService.getCurTokenDisplayIdLocked(), 131 mService.getCurTokenLocked(), 132 mService); 133 curMethod.onCreateInlineSuggestionsRequest( 134 mPendingInlineSuggestionsRequest.mRequestInfo, callback); 135 } else { 136 Slog.w(TAG, "No IME connected! Abandoning inline suggestions creation request."); 137 } 138 clearPendingInlineSuggestionsRequest(); 139 } 140 141 @GuardedBy("ImfLock.class") clearPendingInlineSuggestionsRequest()142 private void clearPendingInlineSuggestionsRequest() { 143 mPendingInlineSuggestionsRequest = null; 144 } 145 isInlineSuggestionsEnabled(InputMethodInfo imi, boolean touchExplorationEnabled)146 private static boolean isInlineSuggestionsEnabled(InputMethodInfo imi, 147 boolean touchExplorationEnabled) { 148 return imi.isInlineSuggestionsEnabled() 149 && (!touchExplorationEnabled 150 || imi.supportsInlineSuggestionsWithTouchExploration()); 151 } 152 153 @GuardedBy("ImfLock.class") invalidateAutofillSession()154 void invalidateAutofillSession() { 155 if (mInlineSuggestionsRequestCallback != null) { 156 try { 157 mInlineSuggestionsRequestCallback.onInlineSuggestionsSessionInvalidated(); 158 } catch (RemoteException e) { 159 Slog.e(TAG, "Cannot invalidate autofill session.", e); 160 } 161 } 162 } 163 164 /** 165 * The decorator which validates the host package name in the 166 * {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name. 167 */ 168 private static final class InlineSuggestionsRequestCallbackDecorator 169 extends IInlineSuggestionsRequestCallback.Stub { 170 @NonNull private final IInlineSuggestionsRequestCallback mCallback; 171 @NonNull private final String mImePackageName; 172 private final int mImeDisplayId; 173 @NonNull private final IBinder mImeToken; 174 @NonNull private final InputMethodManagerService mImms; 175 InlineSuggestionsRequestCallbackDecorator( @onNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName, int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms)176 InlineSuggestionsRequestCallbackDecorator( 177 @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName, 178 int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms) { 179 mCallback = callback; 180 mImePackageName = imePackageName; 181 mImeDisplayId = displayId; 182 mImeToken = imeToken; 183 mImms = imms; 184 } 185 186 @Override onInlineSuggestionsUnsupported()187 public void onInlineSuggestionsUnsupported() throws RemoteException { 188 mCallback.onInlineSuggestionsUnsupported(); 189 } 190 191 @Override onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)192 public void onInlineSuggestionsRequest(InlineSuggestionsRequest request, 193 IInlineSuggestionsResponseCallback callback) 194 throws RemoteException { 195 if (!mImePackageName.equals(request.getHostPackageName())) { 196 throw new SecurityException( 197 "Host package name in the provide request=[" + request.getHostPackageName() 198 + "] doesn't match the IME package name=[" + mImePackageName 199 + "]."); 200 } 201 request.setHostDisplayId(mImeDisplayId); 202 mImms.setCurHostInputToken(mImeToken, request.getHostInputToken()); 203 mCallback.onInlineSuggestionsRequest(request, callback); 204 } 205 206 @Override onInputMethodStartInput(AutofillId imeFieldId)207 public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException { 208 mCallback.onInputMethodStartInput(imeFieldId); 209 } 210 211 @Override onInputMethodShowInputRequested(boolean requestResult)212 public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException { 213 mCallback.onInputMethodShowInputRequested(requestResult); 214 } 215 216 @Override onInputMethodStartInputView()217 public void onInputMethodStartInputView() throws RemoteException { 218 mCallback.onInputMethodStartInputView(); 219 } 220 221 @Override onInputMethodFinishInputView()222 public void onInputMethodFinishInputView() throws RemoteException { 223 mCallback.onInputMethodFinishInputView(); 224 } 225 226 @Override onInputMethodFinishInput()227 public void onInputMethodFinishInput() throws RemoteException { 228 mCallback.onInputMethodFinishInput(); 229 } 230 231 @Override onInlineSuggestionsSessionInvalidated()232 public void onInlineSuggestionsSessionInvalidated() throws RemoteException { 233 mCallback.onInlineSuggestionsSessionInvalidated(); 234 } 235 } 236 } 237