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 357 /** 358 * Handles the IME session status received from the IME. 359 * 360 * <p> Should only be invoked in the {@link #mHandler} thread. 361 */ handleOnReceiveImeSessionInvalidated()362 private void handleOnReceiveImeSessionInvalidated() { 363 synchronized (mLock) { 364 if (mDestroyed) { 365 return; 366 } 367 mImeSessionInvalidated = true; 368 } 369 } 370 isImeShowing()371 boolean isImeShowing() { 372 synchronized (mLock) { 373 return !mDestroyed && mImeShowing; 374 } 375 } 376 377 /** 378 * Internal implementation of {@link IInlineSuggestionsRequestCallback}. 379 */ 380 private static final class InlineSuggestionsRequestCallbackImpl 381 implements InlineSuggestionsRequestCallback { 382 383 private final WeakReference<AutofillInlineSuggestionsRequestSession> mSession; 384 InlineSuggestionsRequestCallbackImpl( AutofillInlineSuggestionsRequestSession session)385 private InlineSuggestionsRequestCallbackImpl( 386 AutofillInlineSuggestionsRequestSession session) { 387 mSession = new WeakReference<>(session); 388 } 389 390 @BinderThread 391 @Override onInlineSuggestionsUnsupported()392 public void onInlineSuggestionsUnsupported() { 393 if (sDebug) Slog.d(TAG, "onInlineSuggestionsUnsupported() called."); 394 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 395 if (session != null) { 396 session.mHandler.sendMessage(obtainMessage( 397 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session, 398 null, null)); 399 } 400 } 401 402 @BinderThread 403 @Override onInlineSuggestionsRequest(InlineSuggestionsRequest request, IInlineSuggestionsResponseCallback callback)404 public void onInlineSuggestionsRequest(InlineSuggestionsRequest request, 405 IInlineSuggestionsResponseCallback callback) { 406 if (sDebug) Slog.d(TAG, "onInlineSuggestionsRequest() received: " + request); 407 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 408 if (session != null) { 409 session.mHandler.sendMessage(obtainMessage( 410 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeRequest, session, 411 request, callback)); 412 } 413 } 414 415 @Override onInputMethodStartInput(AutofillId imeFieldId)416 public void onInputMethodStartInput(AutofillId imeFieldId) { 417 if (sVerbose) Slog.v(TAG, "onInputMethodStartInput() received on " + imeFieldId); 418 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 419 if (session != null) { 420 session.mHandler.sendMessage(obtainMessage( 421 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 422 session, imeFieldId, true, false)); 423 } 424 } 425 426 @Override onInputMethodShowInputRequested(boolean requestResult)427 public void onInputMethodShowInputRequested(boolean requestResult) { 428 if (sVerbose) { 429 Slog.v(TAG, "onInputMethodShowInputRequested() received: " + requestResult); 430 } 431 } 432 433 @BinderThread 434 @Override onInputMethodStartInputView()435 public void onInputMethodStartInputView() { 436 if (sVerbose) Slog.v(TAG, "onInputMethodStartInputView() received"); 437 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 438 if (session != null) { 439 session.mHandler.sendMessage(obtainMessage( 440 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 441 session, true, true)); 442 } 443 } 444 445 @BinderThread 446 @Override onInputMethodFinishInputView()447 public void onInputMethodFinishInputView() { 448 if (sVerbose) Slog.v(TAG, "onInputMethodFinishInputView() received"); 449 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 450 if (session != null) { 451 session.mHandler.sendMessage(obtainMessage( 452 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 453 session, true, false)); 454 } 455 } 456 457 @Override onInputMethodFinishInput()458 public void onInputMethodFinishInput() { 459 if (sVerbose) Slog.v(TAG, "onInputMethodFinishInput() received"); 460 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 461 if (session != null) { 462 session.mHandler.sendMessage(obtainMessage( 463 AutofillInlineSuggestionsRequestSession::handleOnReceiveImeStatusUpdated, 464 session, false, false)); 465 } 466 } 467 468 @BinderThread 469 @Override onInlineSuggestionsSessionInvalidated()470 public void onInlineSuggestionsSessionInvalidated() { 471 if (sDebug) Slog.d(TAG, "onInlineSuggestionsSessionInvalidated() called."); 472 final AutofillInlineSuggestionsRequestSession session = mSession.get(); 473 if (session != null) { 474 session.mHandler.sendMessage(obtainMessage( 475 AutofillInlineSuggestionsRequestSession 476 ::handleOnReceiveImeSessionInvalidated, session)); 477 } 478 } 479 } 480 match(@ullable AutofillId autofillId, @Nullable AutofillId imeClientFieldId)481 private static boolean match(@Nullable AutofillId autofillId, 482 @Nullable AutofillId imeClientFieldId) { 483 // The IME doesn't have information about the virtual view id for the child views in the 484 // web view, so we are only comparing the parent view id here. This means that for cases 485 // where there are two input fields in the web view, they will have the same view id 486 // (although different virtual child id), and we will not be able to distinguish them. 487 return autofillId != null && imeClientFieldId != null 488 && autofillId.getViewId() == imeClientFieldId.getViewId(); 489 } 490 } 491