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