1 /* 2 * Copyright (C) 2020 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.autofill; 18 19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 20 import static com.android.server.autofill.Helper.sDebug; 21 import static com.android.server.autofill.Helper.sVerbose; 22 23 import android.annotation.BinderThread; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.ComponentName; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.RemoteException; 30 import android.util.Slog; 31 import android.view.autofill.AutofillId; 32 import android.view.inputmethod.InlineSuggestion; 33 import android.view.inputmethod.InlineSuggestionsRequest; 34 import android.view.inputmethod.InlineSuggestionsResponse; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.view.IInlineSuggestionsRequestCallback; 38 import com.android.internal.view.IInlineSuggestionsResponseCallback; 39 import com.android.internal.view.InlineSuggestionsRequestInfo; 40 import com.android.server.autofill.ui.InlineFillUi; 41 import com.android.server.inputmethod.InputMethodManagerInternal; 42 43 import java.lang.ref.WeakReference; 44 import java.util.List; 45 import java.util.Optional; 46 import java.util.function.Consumer; 47 48 /** 49 * Maintains an inline suggestion session with the IME. 50 * 51 * <p> Each session corresponds to one request from the Autofill manager service to create an 52 * {@link InlineSuggestionsRequest}. It's responsible for receiving callbacks from the IME and 53 * sending {@link android.view.inputmethod.InlineSuggestionsResponse} to IME. 54 */ 55 final class AutofillInlineSuggestionsRequestSession { 56 57 private static final String TAG = AutofillInlineSuggestionsRequestSession.class.getSimpleName(); 58 59 @NonNull 60 private final InputMethodManagerInternal mInputMethodManagerInternal; 61 private final int mUserId; 62 @NonNull 63 private final ComponentName mComponentName; 64 @NonNull 65 private final Object mLock; 66 @NonNull 67 private final Handler mHandler; 68 @NonNull 69 private final Bundle mUiExtras; 70 @NonNull 71 private final InlineFillUi.InlineUiEventCallback mUiCallback; 72 73 @GuardedBy("mLock") 74 @NonNull 75 private AutofillId mAutofillId; 76 @GuardedBy("mLock") 77 @Nullable 78 private Consumer<InlineSuggestionsRequest> mImeRequestConsumer; 79 80 @GuardedBy("mLock") 81 private boolean mImeRequestReceived; 82 @GuardedBy("mLock") 83 @Nullable 84 private InlineSuggestionsRequest mImeRequest; 85 @GuardedBy("mLock") 86 @Nullable 87 private IInlineSuggestionsResponseCallback mResponseCallback; 88 89 @GuardedBy("mLock") 90 @Nullable 91 private AutofillId mImeCurrentFieldId; 92 @GuardedBy("mLock") 93 private boolean mImeInputStarted; 94 @GuardedBy("mLock") 95 private boolean mImeInputViewStarted; 96 @GuardedBy("mLock") 97 @Nullable 98 private InlineFillUi mInlineFillUi; 99 @GuardedBy("mLock") 100 private Boolean mPreviousResponseIsNotEmpty = null; 101 102 @GuardedBy("mLock") 103 private boolean mDestroyed = false; 104 @GuardedBy("mLock") 105 private boolean mPreviousHasNonPinSuggestionShow; 106 @GuardedBy("mLock") 107 private boolean mImeSessionInvalidated = false; 108 AutofillInlineSuggestionsRequestSession( @onNull 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)109 AutofillInlineSuggestionsRequestSession( 110 @NonNull InputMethodManagerInternal inputMethodManagerInternal, int userId, 111 @NonNull ComponentName componentName, @NonNull Handler handler, @NonNull Object lock, 112 @NonNull AutofillId autofillId, 113 @NonNull Consumer<InlineSuggestionsRequest> requestConsumer, @NonNull Bundle uiExtras, 114 @NonNull InlineFillUi.InlineUiEventCallback callback) { 115 mInputMethodManagerInternal = inputMethodManagerInternal; 116 mUserId = userId; 117 mComponentName = componentName; 118 mHandler = handler; 119 mLock = lock; 120 mUiExtras = uiExtras; 121 mUiCallback = callback; 122 123 mAutofillId = autofillId; 124 mImeRequestConsumer = requestConsumer; 125 } 126 127 @GuardedBy("mLock") 128 @NonNull getAutofillIdLocked()129 AutofillId getAutofillIdLocked() { 130 return mAutofillId; 131 } 132 133 /** 134 * Returns the {@link InlineSuggestionsRequest} provided by IME. 135 * 136 * <p> The caller is responsible for making sure Autofill hears back from IME before calling 137 * this method, using the {@link #mImeRequestConsumer}. 138 */ 139 @GuardedBy("mLock") getInlineSuggestionsRequestLocked()140 Optional<InlineSuggestionsRequest> getInlineSuggestionsRequestLocked() { 141 if (mDestroyed) { 142 return Optional.empty(); 143 } 144 return Optional.ofNullable(mImeRequest); 145 } 146 147 /** 148 * Requests showing the inline suggestion in the IME when the IME becomes visible and is focused 149 * on the {@code autofillId}. 150 * 151 * @return false if the IME callback is not available. 152 */ 153 @GuardedBy("mLock") onInlineSuggestionsResponseLocked(@onNull InlineFillUi inlineFillUi)154 boolean onInlineSuggestionsResponseLocked(@NonNull InlineFillUi inlineFillUi) { 155 if (mDestroyed) { 156 return false; 157 } 158 if (sDebug) { 159 Slog.d(TAG, 160 "onInlineSuggestionsResponseLocked called for:" + inlineFillUi.getAutofillId()); 161 } 162 if (mImeRequest == null || mResponseCallback == null || mImeSessionInvalidated) { 163 return false; 164 } 165 // TODO(b/151123764): each session should only correspond to one field. 166 mAutofillId = inlineFillUi.getAutofillId(); 167 mInlineFillUi = inlineFillUi; 168 maybeUpdateResponseToImeLocked(); 169 return true; 170 } 171 172 /** 173 * Prevents further interaction with the IME. Must be called before starting a new request 174 * session to avoid unwanted behavior from two overlapping requests. 175 */ 176 @GuardedBy("mLock") destroySessionLocked()177 void destroySessionLocked() { 178 mDestroyed = true; 179 180 if (!mImeRequestReceived) { 181 Slog.w(TAG, 182 "Never received an InlineSuggestionsRequest from the IME for " + mAutofillId); 183 } 184 } 185 186 /** 187 * Requests the IME to create an {@link InlineSuggestionsRequest}. 188 * 189 * <p> This method should only be called once per session. 190 */ 191 @GuardedBy("mLock") onCreateInlineSuggestionsRequestLocked()192 void onCreateInlineSuggestionsRequestLocked() { 193 if (mDestroyed) { 194 return; 195 } 196 mImeSessionInvalidated = false; 197 if (sDebug) Slog.d(TAG, "onCreateInlineSuggestionsRequestLocked called: " + mAutofillId); 198 mInputMethodManagerInternal.onCreateInlineSuggestionsRequest(mUserId, 199 new InlineSuggestionsRequestInfo(mComponentName, mAutofillId, mUiExtras), 200 new InlineSuggestionsRequestCallbackImpl(this)); 201 } 202 203 /** 204 * Clear the locally cached inline fill UI, but don't clear the suggestion in IME. 205 * 206 * See also {@link AutofillInlineSessionController#resetInlineFillUiLocked()} 207 */ 208 @GuardedBy("mLock") resetInlineFillUiLocked()209 void resetInlineFillUiLocked() { 210 mInlineFillUi = null; 211 } 212 213 /** 214 * Optionally sends inline response to the IME, depending on the current state. 215 */ 216 @GuardedBy("mLock") maybeUpdateResponseToImeLocked()217 private void maybeUpdateResponseToImeLocked() { 218 if (sVerbose) Slog.v(TAG, "maybeUpdateResponseToImeLocked called"); 219 if (mDestroyed || mResponseCallback == null) { 220 return; 221 } 222 if (mImeInputViewStarted && mInlineFillUi != null && match(mAutofillId, 223 mImeCurrentFieldId)) { 224 // if IME is visible, and response is not null, send the response 225 InlineSuggestionsResponse response = mInlineFillUi.getInlineSuggestionsResponse(); 226 boolean isEmptyResponse = response.getInlineSuggestions().isEmpty(); 227 if (isEmptyResponse && Boolean.FALSE.equals(mPreviousResponseIsNotEmpty)) { 228 // No-op if both the previous response and current response are empty. 229 return; 230 } 231 maybeNotifyFillUiEventLocked(response.getInlineSuggestions()); 232 updateResponseToImeUncheckLocked(response); 233 mPreviousResponseIsNotEmpty = !isEmptyResponse; 234 } 235 } 236 237 /** 238 * Sends the {@code response} to the IME, assuming all the relevant checks are already done. 239 */ 240 @GuardedBy("mLock") updateResponseToImeUncheckLocked(InlineSuggestionsResponse response)241 private void updateResponseToImeUncheckLocked(InlineSuggestionsResponse response) { 242 if (mDestroyed) { 243 return; 244 } 245 if (sDebug) Slog.d(TAG, "Send inline response: " + response.getInlineSuggestions().size()); 246 try { 247 mResponseCallback.onInlineSuggestionsResponse(mAutofillId, response); 248 } catch (RemoteException e) { 249 Slog.e(TAG, "RemoteException sending InlineSuggestionsResponse to IME"); 250 } 251 } 252 253 @GuardedBy("mLock") maybeNotifyFillUiEventLocked(@onNull List<InlineSuggestion> suggestions)254 private void maybeNotifyFillUiEventLocked(@NonNull List<InlineSuggestion> suggestions) { 255 if (mDestroyed) { 256 return; 257 } 258 boolean hasSuggestionToShow = false; 259 for (int i = 0; i < suggestions.size(); i++) { 260 InlineSuggestion suggestion = suggestions.get(i); 261 // It is possible we don't have any match result but we still have pinned 262 // suggestions. Only notify we have non-pinned suggestions to show 263 if (!suggestion.getInfo().isPinned()) { 264 hasSuggestionToShow = true; 265 break; 266 } 267 } 268 if (sDebug) { 269 Slog.d(TAG, "maybeNotifyFillUiEventLoked(): hasSuggestionToShow=" + hasSuggestionToShow 270 + ", mPreviousHasNonPinSuggestionShow=" + mPreviousHasNonPinSuggestionShow); 271 } 272 // Use mPreviousHasNonPinSuggestionShow to save previous status, if the display status 273 // change, we can notify the event. 274 if (hasSuggestionToShow && !mPreviousHasNonPinSuggestionShow) { 275 // From no suggestion to has suggestions to show 276 mUiCallback.notifyInlineUiShown(mAutofillId); 277 } else if (!hasSuggestionToShow && mPreviousHasNonPinSuggestionShow) { 278 // From has suggestions to no suggestions to show 279 mUiCallback.notifyInlineUiHidden(mAutofillId); 280 } 281 // Update the latest status 282 mPreviousHasNonPinSuggestionShow = hasSuggestionToShow; 283 } 284 285 /** 286 * Handles the {@code request} and {@code callback} received from the IME. 287 * 288 * <p> Should only invoked in the {@link #mHandler} thread. 289 */ handleOnReceiveImeRequest(@ullable InlineSuggestionsRequest request, @Nullable IInlineSuggestionsResponseCallback callback)290 private void handleOnReceiveImeRequest(@Nullable InlineSuggestionsRequest request, 291 @Nullable IInlineSuggestionsResponseCallback callback) { 292 synchronized (mLock) { 293 if (mDestroyed || mImeRequestReceived) { 294 return; 295 } 296 mImeRequestReceived = true; 297 mImeSessionInvalidated = false; 298 299 if (request != null && callback != null) { 300 mImeRequest = request; 301 mResponseCallback = callback; 302 handleOnReceiveImeStatusUpdated(mAutofillId, true, false); 303 } 304 if (mImeRequestConsumer != null) { 305 // Note that mImeRequest is only set if both request and callback are non-null. 306 mImeRequestConsumer.accept(mImeRequest); 307 mImeRequestConsumer = null; 308 } 309 } 310 } 311 312 /** 313 * Handles the IME status updates received from the IME. 314 * 315 * <p> Should only be invoked in the {@link #mHandler} thread. 316 */ handleOnReceiveImeStatusUpdated(boolean imeInputStarted, boolean imeInputViewStarted)317 private void handleOnReceiveImeStatusUpdated(boolean imeInputStarted, 318 boolean imeInputViewStarted) { 319 synchronized (mLock) { 320 if (mDestroyed) { 321 return; 322 } 323 if (mImeCurrentFieldId != null) { 324 boolean imeInputStartedChanged = (mImeInputStarted != imeInputStarted); 325 boolean imeInputViewStartedChanged = (mImeInputViewStarted != imeInputViewStarted); 326 mImeInputStarted = imeInputStarted; 327 mImeInputViewStarted = imeInputViewStarted; 328 if (imeInputStartedChanged || imeInputViewStartedChanged) { 329 maybeUpdateResponseToImeLocked(); 330 } 331 } 332 } 333 } 334 335 /** 336 * Handles the IME status updates received from the IME. 337 * 338 * <p> Should only be invoked in the {@link #mHandler} thread. 339 */ handleOnReceiveImeStatusUpdated(@ullable AutofillId imeFieldId, boolean imeInputStarted, boolean imeInputViewStarted)340 private void handleOnReceiveImeStatusUpdated(@Nullable AutofillId imeFieldId, 341 boolean imeInputStarted, boolean imeInputViewStarted) { 342 synchronized (mLock) { 343 if (mDestroyed) { 344 return; 345 } 346 if (imeFieldId != null) { 347 mImeCurrentFieldId = imeFieldId; 348 } 349 handleOnReceiveImeStatusUpdated(imeInputStarted, imeInputViewStarted); 350 } 351 } 352 353 /** 354 * Handles the IME session status received from the IME. 355 * 356 * <p> Should only be invoked in the {@link #mHandler} thread. 357 */ handleOnReceiveImeSessionInvalidated()358 private void handleOnReceiveImeSessionInvalidated() { 359 synchronized (mLock) { 360 if (mDestroyed) { 361 return; 362 } 363 mImeSessionInvalidated = true; 364 } 365 } 366 367 /** 368 * Internal implementation of {@link IInlineSuggestionsRequestCallback}. 369 */ 370 private static final class InlineSuggestionsRequestCallbackImpl extends 371 IInlineSuggestionsRequestCallback.Stub { 372 373 private final WeakReference<AutofillInlineSuggestionsRequestSession> mSession; 374 InlineSuggestionsRequestCallbackImpl( AutofillInlineSuggestionsRequestSession session)375 private InlineSuggestionsRequestCallbackImpl( 376 AutofillInlineSuggestionsRequestSession session) { 377 mSession = new WeakReference<>(session); 378 } 379 380 @BinderThread 381 @Override onInlineSuggestionsUnsupported()382 public void onInlineSuggestionsUnsupported() throws RemoteException { 383 if (sDebug) Slog.d(TAG, "onInlineSuggestionsUnsupported() called."); 384 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 385 if (session != null) { 386 session.mHandler.sendMessage(obtainMessage( 387 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session, 388 null, null)); 389 } 390 } 391 392 @BinderThread 393 @Override onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)394 public void onInlineSuggestionsRequest(InlineSuggestionsRequest request, 395 IInlineSuggestionsResponseCallback callback) { 396 if (sDebug) Slog.d(TAG, "onInlineSuggestionsRequest() received: " + request); 397 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 398 if (session != null) { 399 session.mHandler.sendMessage(obtainMessage( 400 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session, 401 request, callback)); 402 } 403 } 404 405 @Override onInputMethodStartInput(AutofillId imeFieldId)406 public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException { 407 if (sVerbose) Slog.v(TAG, "onInputMethodStartInput() received on " + imeFieldId); 408 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 409 if (session != null) { 410 session.mHandler.sendMessage(obtainMessage( 411 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 412 session, imeFieldId, true, false)); 413 } 414 } 415 416 @Override onInputMethodShowInputRequested(boolean requestResult)417 public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException { 418 if (sVerbose) { 419 Slog.v(TAG, "onInputMethodShowInputRequested() received: " + requestResult); 420 } 421 } 422 423 @BinderThread 424 @Override onInputMethodStartInputView()425 public void onInputMethodStartInputView() { 426 if (sVerbose) Slog.v(TAG, "onInputMethodStartInputView() received"); 427 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 428 if (session != null) { 429 session.mHandler.sendMessage(obtainMessage( 430 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 431 session, true, true)); 432 } 433 } 434 435 @BinderThread 436 @Override onInputMethodFinishInputView()437 public void onInputMethodFinishInputView() { 438 if (sVerbose) Slog.v(TAG, "onInputMethodFinishInputView() received"); 439 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 440 if (session != null) { 441 session.mHandler.sendMessage(obtainMessage( 442 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 443 session, true, false)); 444 } 445 } 446 447 @Override onInputMethodFinishInput()448 public void onInputMethodFinishInput() throws RemoteException { 449 if (sVerbose) Slog.v(TAG, "onInputMethodFinishInput() received"); 450 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 451 if (session != null) { 452 session.mHandler.sendMessage(obtainMessage( 453 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 454 session, false, false)); 455 } 456 } 457 458 @BinderThread 459 @Override onInlineSuggestionsSessionInvalidated()460 public void onInlineSuggestionsSessionInvalidated() throws RemoteException { 461 if (sDebug) Slog.d(TAG, "onInlineSuggestionsSessionInvalidated() called."); 462 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 463 if (session != null) { 464 session.mHandler.sendMessage(obtainMessage( 465 AutofillInlineSuggestionsRequestSession 466 ::handleOnReceiveImeSessionInvalidated, session)); 467 } 468 } 469 } 470 match(@ullable AutofillId autofillId, @Nullable AutofillId imeClientFieldId)471 private static boolean match(@Nullable AutofillId autofillId, 472 @Nullable AutofillId imeClientFieldId) { 473 // The IME doesn't have information about the virtual view id for the child views in the 474 // web view, so we are only comparing the parent view id here. This means that for cases 475 // where there are two input fields in the web view, they will have the same view id 476 // (although different virtual child id), and we will not be able to distinguish them. 477 return autofillId != null && imeClientFieldId != null 478 && autofillId.getViewId() == imeClientFieldId.getViewId(); 479 } 480 } 481