1 /* 2 * Copyright (C) 2021 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 android.view.autofill; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.app.ActivityOptions; 23 import android.app.Application; 24 import android.content.ComponentName; 25 import android.content.Intent; 26 import android.content.IntentSender; 27 import android.graphics.Rect; 28 import android.os.Bundle; 29 import android.os.IBinder; 30 import android.text.TextUtils; 31 import android.util.Dumpable; 32 import android.util.Log; 33 import android.util.Slog; 34 import android.view.KeyEvent; 35 import android.view.View; 36 import android.view.ViewRootImpl; 37 import android.view.WindowManagerGlobal; 38 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 44 /** 45 * A controller to manage the autofill requests for the {@link Activity}. 46 * 47 * @hide 48 */ 49 public final class AutofillClientController implements AutofillManager.AutofillClient, Dumpable { 50 51 private static final String TAG = "AutofillClientController"; 52 53 private static final String LOG_TAG = "autofill_client"; 54 public static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); 55 56 public static final String LAST_AUTOFILL_ID = "android:lastAutofillId"; 57 public static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded"; 58 public static final String AUTO_FILL_AUTH_WHO_PREFIX = "@android:autoFillAuth:"; 59 60 public static final String DUMPABLE_NAME = "AutofillManager"; 61 62 /** The last autofill id that was returned from {@link #getNextAutofillId()} */ 63 public int mLastAutofillId = View.LAST_APP_AUTOFILL_ID; 64 65 @NonNull 66 private final Activity mActivity; 67 /** The autofill manager. Always access via {@link #getAutofillManager()}. */ 68 @Nullable 69 private AutofillManager mAutofillManager; 70 /** The autofill dropdown fill ui. */ 71 @Nullable 72 private AutofillPopupWindow mAutofillPopupWindow; 73 private boolean mAutoFillResetNeeded; 74 private boolean mAutoFillIgnoreFirstResumePause; 75 private Boolean mRelayoutFix; 76 77 /** 78 * AutofillClientController constructor. 79 */ AutofillClientController(Activity activity)80 public AutofillClientController(Activity activity) { 81 mActivity = activity; 82 } 83 getAutofillManager()84 private AutofillManager getAutofillManager() { 85 if (mAutofillManager == null) { 86 mAutofillManager = mActivity.getSystemService(AutofillManager.class); 87 } 88 return mAutofillManager; 89 } 90 91 /** 92 * Whether to apply relayout fixes. 93 * 94 * @hide 95 */ isRelayoutFixEnabled()96 public boolean isRelayoutFixEnabled() { 97 AutofillManager autofillManager = getAutofillManager(); 98 if (autofillManager == null) { 99 if (Helper.sDebug) { 100 Log.d(TAG, "isRelayoutFixEnabled() : getAutofillManager() == null"); 101 } 102 return false; 103 } 104 if (mRelayoutFix == null) { 105 mRelayoutFix = autofillManager.isRelayoutFixEnabled(); 106 } 107 return mRelayoutFix; 108 } 109 110 // ------------------ Called for Activity events ------------------ 111 112 /** 113 * Called when the Activity is attached. 114 */ onActivityAttached(Application application)115 public void onActivityAttached(Application application) { 116 mActivity.setAutofillOptions(application.getAutofillOptions()); 117 } 118 119 /** 120 * Called when the {@link Activity#onCreate(Bundle)} is called. 121 */ onActivityCreated(@onNull Bundle savedInstanceState)122 public void onActivityCreated(@NonNull Bundle savedInstanceState) { 123 mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false); 124 mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID, View.LAST_APP_AUTOFILL_ID); 125 if (mAutoFillResetNeeded) { 126 getAutofillManager().onCreate(savedInstanceState); 127 } 128 } 129 130 /** 131 * Called when the {@link Activity#onStart()} is called. 132 */ onActivityStarted()133 public void onActivityStarted() { 134 if (mAutoFillResetNeeded) { 135 getAutofillManager().onVisibleForAutofill(); 136 } 137 } 138 139 /** 140 * Called when the {@link Activity#onResume()} is called. 141 */ onActivityResumed()142 public void onActivityResumed() { 143 if (Helper.sVerbose) { 144 Log.v(TAG, "onActivityResumed()"); 145 } 146 if (isRelayoutFixEnabled()) { 147 // Do nothing here. We'll handle it in onActivityPostResumed() 148 return; 149 } 150 if (Helper.sVerbose) { 151 Log.v(TAG, "onActivityResumed(): Relayout fix not enabled"); 152 } 153 forResume(); 154 } 155 156 /** 157 * Called when the {@link Activity#onPostResume()} is called. 158 */ onActivityPostResumed()159 public void onActivityPostResumed() { 160 if (Helper.sVerbose) { 161 Log.v(TAG, "onActivityPostResumed()"); 162 } 163 if (!isRelayoutFixEnabled()) { 164 return; 165 } 166 if (Helper.sVerbose) { 167 Log.v(TAG, "onActivityPostResumed(): Relayout fix enabled"); 168 } 169 forResume(); 170 } 171 172 /** 173 * Code to execute when an app has resumed (or is about to resume) 174 */ forResume()175 private void forResume() { 176 enableAutofillCompatibilityIfNeeded(); 177 boolean relayoutFix = isRelayoutFixEnabled(); 178 if (relayoutFix) { 179 if (getAutofillManager().shouldRetryFill()) { 180 if (Helper.sVerbose) { 181 Log.v(TAG, "forResume(): Autofill potential relayout. Retrying fill."); 182 } 183 getAutofillManager().attemptRefill(); 184 } else { 185 if (Helper.sVerbose) { 186 Log.v(TAG, "forResume(): Not attempting refill."); 187 } 188 } 189 } 190 191 if (mAutoFillResetNeeded) { 192 if (!mAutoFillIgnoreFirstResumePause) { 193 View focus = mActivity.getCurrentFocus(); 194 if (focus != null && focus.canNotifyAutofillEnterExitEvent()) { 195 // TODO(b/148815880): Bring up keyboard if resumed from inline authentication. 196 // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest# 197 // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial 198 // window visibility after recreation is INVISIBLE in onResume() and next frame 199 // ViewRootImpl.performTraversals() changes window visibility to VISIBLE. 200 // So we cannot call View.notifyEnterOrExited() which will do nothing 201 // when View.isVisibleToUser() is false. 202 if (relayoutFix && getAutofillManager().isAuthenticationPending()) { 203 if (Helper.sVerbose) { 204 Log.v(TAG, "forResume(): ignoring focus due to auth pending"); 205 } 206 } else { 207 if (Helper.sVerbose) { 208 Log.v(TAG, "forResume(): notifyViewEntered"); 209 } 210 getAutofillManager().notifyViewEntered(focus); 211 } 212 } 213 } 214 } 215 } 216 217 /** 218 * Called when the Activity is performing resume. 219 */ onActivityPerformResume(boolean followedByPause)220 public void onActivityPerformResume(boolean followedByPause) { 221 if (mAutoFillResetNeeded) { 222 // When Activity is destroyed in paused state, and relaunch activity, there will be 223 // extra onResume and onPause event, ignore the first onResume and onPause. 224 // see ActivityThread.handleRelaunchActivity() 225 mAutoFillIgnoreFirstResumePause = followedByPause; 226 if (mAutoFillIgnoreFirstResumePause && DEBUG) { 227 Slog.v(TAG, "autofill will ignore first pause when relaunching " + this); 228 } 229 } 230 } 231 232 /** 233 * Called when the {@link Activity#onPause()} is called. 234 */ onActivityPaused()235 public void onActivityPaused() { 236 if (mAutoFillResetNeeded) { 237 if (!mAutoFillIgnoreFirstResumePause) { 238 if (DEBUG) Log.v(TAG, "autofill notifyViewExited " + this); 239 View focus = mActivity.getCurrentFocus(); 240 if (focus != null && focus.canNotifyAutofillEnterExitEvent()) { 241 getAutofillManager().notifyViewExited(focus); 242 } 243 } else { 244 // reset after first pause() 245 if (DEBUG) Log.v(TAG, "autofill got first pause " + this); 246 mAutoFillIgnoreFirstResumePause = false; 247 } 248 } 249 } 250 251 /** 252 * Called when the {@link Activity#onStop()} is called. 253 */ onActivityStopped(Intent intent, boolean changingConfigurations)254 public void onActivityStopped(Intent intent, boolean changingConfigurations) { 255 if (mAutoFillResetNeeded) { 256 // If stopped without changing the configurations, the response should expire. 257 getAutofillManager().onInvisibleForAutofill(!changingConfigurations); 258 } else if (intent != null 259 && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN) 260 && intent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) { 261 restoreAutofillSaveUi(intent); 262 } 263 } 264 265 /** 266 * Called when the {@link Activity#onDestroy()} is called. 267 */ onActivityDestroyed()268 public void onActivityDestroyed() { 269 if (mActivity.isFinishing() && mAutoFillResetNeeded) { 270 getAutofillManager().onActivityFinishing(); 271 } 272 } 273 274 /** 275 * Called when the {@link Activity#onSaveInstanceState(Bundle)} is called. 276 */ onSaveInstanceState(Bundle outState)277 public void onSaveInstanceState(Bundle outState) { 278 outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId); 279 if (mAutoFillResetNeeded) { 280 outState.putBoolean(AUTOFILL_RESET_NEEDED, true); 281 getAutofillManager().onSaveInstanceState(outState); 282 } 283 } 284 285 /** 286 * Called when the {@link Activity#finish()} is called. 287 */ onActivityFinish(Intent intent)288 public void onActivityFinish(Intent intent) { 289 // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must 290 // be restored now. 291 if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { 292 restoreAutofillSaveUi(intent); 293 } 294 } 295 296 /** 297 * Called when the {@link Activity#onBackPressed()} is called. 298 */ onActivityBackPressed(Intent intent)299 public void onActivityBackPressed(Intent intent) { 300 // Activity was launched when user tapped a link in the Autofill Save UI - Save UI must 301 // be restored now. 302 if (intent != null && intent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { 303 restoreAutofillSaveUi(intent); 304 } 305 } 306 307 /** 308 * Called when the Activity is dispatching the result. 309 */ onDispatchActivityResult(int requestCode, int resultCode, Intent data)310 public void onDispatchActivityResult(int requestCode, int resultCode, Intent data) { 311 Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null; 312 getAutofillManager().onAuthenticationResult(requestCode, resultData, 313 mActivity.getCurrentFocus()); 314 } 315 316 /** 317 * Called when the {@link Activity#startActivity(Intent, Bundle)} is called. 318 */ onStartActivity(Intent startIntent, Intent cachedIntent)319 public void onStartActivity(Intent startIntent, Intent cachedIntent) { 320 if (cachedIntent != null 321 && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN) 322 && cachedIntent.hasExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY)) { 323 if (TextUtils.equals(mActivity.getPackageName(), 324 startIntent.resolveActivity(mActivity.getPackageManager()).getPackageName())) { 325 // Apply Autofill restore mechanism on the started activity by startActivity() 326 final IBinder token = 327 cachedIntent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); 328 // Remove restore ability from current activity 329 cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); 330 cachedIntent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY); 331 // Put restore token 332 startIntent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); 333 startIntent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true); 334 } 335 } 336 } 337 338 /** 339 * Restore the autofill save ui. 340 */ restoreAutofillSaveUi(Intent intent)341 public void restoreAutofillSaveUi(Intent intent) { 342 final IBinder token = 343 intent.getIBinderExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); 344 // Make only restore Autofill once 345 intent.removeExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN); 346 intent.removeExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY); 347 getAutofillManager().onPendingSaveUi(AutofillManager.PENDING_UI_OPERATION_RESTORE, 348 token); 349 } 350 351 /** 352 * Enable autofill compatibility mode for the Activity if the compatibility mode is enabled 353 * for the package. 354 */ enableAutofillCompatibilityIfNeeded()355 public void enableAutofillCompatibilityIfNeeded() { 356 if (mActivity.isAutofillCompatibilityEnabled()) { 357 final AutofillManager afm = mActivity.getSystemService(AutofillManager.class); 358 if (afm != null) { 359 afm.enableCompatibilityMode(); 360 } 361 } 362 } 363 364 @Override getDumpableName()365 public String getDumpableName() { 366 return DUMPABLE_NAME; 367 } 368 369 @Override dump(PrintWriter writer, String[] args)370 public void dump(PrintWriter writer, String[] args) { 371 final String prefix = ""; 372 final AutofillManager afm = getAutofillManager(); 373 if (afm != null) { 374 afm.dump(prefix, writer); 375 writer.print(prefix); writer.print("Autofill Compat Mode: "); 376 writer.println(mActivity.isAutofillCompatibilityEnabled()); 377 } else { 378 writer.print(prefix); writer.println("No AutofillManager"); 379 } 380 } 381 382 /** 383 * Returns the next autofill ID that is unique in the activity 384 * 385 * <p>All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned 386 * will be unique. 387 */ getNextAutofillId()388 public int getNextAutofillId() { 389 if (mLastAutofillId == Integer.MAX_VALUE - 1) { 390 mLastAutofillId = View.LAST_APP_AUTOFILL_ID; 391 } 392 393 mLastAutofillId++; 394 395 return mLastAutofillId; 396 } 397 398 // ------------------ AutofillClient implementation ------------------ 399 400 @Override autofillClientGetNextAutofillId()401 public AutofillId autofillClientGetNextAutofillId() { 402 return new AutofillId(getNextAutofillId()); 403 } 404 405 @Override autofillClientIsCompatibilityModeEnabled()406 public boolean autofillClientIsCompatibilityModeEnabled() { 407 return mActivity.isAutofillCompatibilityEnabled(); 408 } 409 410 @Override autofillClientIsVisibleForAutofill()411 public boolean autofillClientIsVisibleForAutofill() { 412 return mActivity.isVisibleForAutofill(); 413 } 414 415 @Override autofillClientGetComponentName()416 public ComponentName autofillClientGetComponentName() { 417 return mActivity.getComponentName(); 418 } 419 420 @Override autofillClientGetActivityToken()421 public IBinder autofillClientGetActivityToken() { 422 return mActivity.getActivityToken(); 423 } 424 425 @Override autofillClientGetViewVisibility(AutofillId[] autofillIds)426 public boolean[] autofillClientGetViewVisibility(AutofillId[] autofillIds) { 427 final int autofillIdCount = autofillIds.length; 428 final boolean[] visible = new boolean[autofillIdCount]; 429 for (int i = 0; i < autofillIdCount; i++) { 430 final AutofillId autofillId = autofillIds[i]; 431 if (autofillId == null) { 432 visible[i] = false; 433 continue; 434 } 435 final View view = autofillClientFindViewByAutofillIdTraversal(autofillId); 436 if (view != null) { 437 if (!autofillId.isVirtualInt()) { 438 visible[i] = view.isVisibleToUser(); 439 } else { 440 visible[i] = view.isVisibleToUserForAutofill(autofillId.getVirtualChildIntId()); 441 } 442 } 443 } 444 if (android.view.autofill.Helper.sVerbose) { 445 Log.v(TAG, "autofillClientGetViewVisibility(): " + Arrays.toString(visible)); 446 } 447 return visible; 448 } 449 450 @Override autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId)451 public View autofillClientFindViewByAccessibilityIdTraversal(int viewId, int windowId) { 452 final ArrayList<ViewRootImpl> roots = WindowManagerGlobal.getInstance() 453 .getRootViews(mActivity.getActivityToken()); 454 for (int rootNum = 0; rootNum < roots.size(); rootNum++) { 455 final View rootView = roots.get(rootNum).getView(); 456 if (rootView != null && rootView.getAccessibilityWindowId() == windowId) { 457 final View view = rootView.findViewByAccessibilityIdTraversal(viewId); 458 if (view != null) { 459 return view; 460 } 461 } 462 } 463 return null; 464 } 465 466 @Override autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId)467 public View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) { 468 if (autofillId == null) return null; 469 final ArrayList<ViewRootImpl> roots = 470 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken()); 471 for (int rootNum = 0; rootNum < roots.size(); rootNum++) { 472 final View rootView = roots.get(rootNum).getView(); 473 474 if (rootView != null) { 475 final View view = rootView.findViewByAutofillIdTraversal(autofillId.getViewId()); 476 if (view != null) { 477 return view; 478 } 479 } 480 } 481 return null; 482 } 483 484 @Override autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds)485 public View[] autofillClientFindViewsByAutofillIdTraversal(AutofillId[] autofillIds) { 486 final View[] views = new View[autofillIds.length]; 487 final ArrayList<ViewRootImpl> roots = 488 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken()); 489 490 for (int rootNum = 0; rootNum < roots.size(); rootNum++) { 491 final View rootView = roots.get(rootNum).getView(); 492 493 if (rootView != null) { 494 final int viewCount = autofillIds.length; 495 for (int viewNum = 0; viewNum < viewCount; viewNum++) { 496 if (autofillIds[viewNum] != null && views[viewNum] == null) { 497 views[viewNum] = rootView.findViewByAutofillIdTraversal( 498 autofillIds[viewNum].getViewId()); 499 } 500 } 501 } 502 } 503 return views; 504 } 505 506 @Override autofillClientFindAutofillableViewsByTraversal()507 public List<View> autofillClientFindAutofillableViewsByTraversal() { 508 final ArrayList<View> views = new ArrayList<>(); 509 final ArrayList<ViewRootImpl> roots = 510 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken()); 511 512 for (int rootNum = 0; rootNum < roots.size(); rootNum++) { 513 final View rootView = roots.get(rootNum).getView(); 514 515 if (rootView != null) { 516 rootView.findAutofillableViewsByTraversal(views); 517 } 518 } 519 return views; 520 } 521 522 @Override autofillClientIsFillUiShowing()523 public boolean autofillClientIsFillUiShowing() { 524 return mAutofillPopupWindow != null && mAutofillPopupWindow.isShowing(); 525 } 526 527 @Override autofillClientRequestHideFillUi()528 public boolean autofillClientRequestHideFillUi() { 529 if (mAutofillPopupWindow == null) { 530 return false; 531 } 532 mAutofillPopupWindow.dismiss(); 533 mAutofillPopupWindow = null; 534 return true; 535 } 536 537 @Override autofillClientRequestShowFillUi(@onNull View anchor, int width, int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter)538 public boolean autofillClientRequestShowFillUi(@NonNull View anchor, int width, 539 int height, @Nullable Rect anchorBounds, IAutofillWindowPresenter presenter) { 540 final boolean wasShowing; 541 542 if (mAutofillPopupWindow == null) { 543 wasShowing = false; 544 mAutofillPopupWindow = new AutofillPopupWindow(presenter); 545 } else { 546 wasShowing = mAutofillPopupWindow.isShowing(); 547 } 548 mAutofillPopupWindow.update(anchor, 0, 0, width, height, anchorBounds); 549 550 return !wasShowing && mAutofillPopupWindow.isShowing(); 551 } 552 553 @Override autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent)554 public void autofillClientDispatchUnhandledKey(View anchor, KeyEvent keyEvent) { 555 ViewRootImpl rootImpl = anchor.getViewRootImpl(); 556 if (rootImpl != null) { 557 // don't care if anchorView is current focus, for example a custom view may only receive 558 // touchEvent, not focusable but can still trigger autofill window. The Key handling 559 // might be inside parent of the custom view. 560 rootImpl.dispatchKeyFromAutofill(keyEvent); 561 } 562 } 563 564 @Override isDisablingEnterExitEventForAutofill()565 public boolean isDisablingEnterExitEventForAutofill() { 566 return mAutoFillIgnoreFirstResumePause || !mActivity.isResumed(); 567 } 568 569 @Override autofillClientResetableStateAvailable()570 public void autofillClientResetableStateAvailable() { 571 mAutoFillResetNeeded = true; 572 } 573 574 @Override autofillClientRunOnUiThread(Runnable action)575 public void autofillClientRunOnUiThread(Runnable action) { 576 mActivity.runOnUiThread(action); 577 } 578 579 @Override autofillClientAuthenticate(int authenticationId, IntentSender intent, Intent fillInIntent, boolean authenticateInline)580 public void autofillClientAuthenticate(int authenticationId, IntentSender intent, 581 Intent fillInIntent, boolean authenticateInline) { 582 try { 583 ActivityOptions activityOptions = ActivityOptions.makeBasic() 584 .setPendingIntentBackgroundActivityStartMode( 585 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); 586 mActivity.startIntentSenderForResult(intent, AUTO_FILL_AUTH_WHO_PREFIX, 587 authenticationId, fillInIntent, 0, 0, activityOptions.toBundle()); 588 } catch (IntentSender.SendIntentException e) { 589 Log.e(TAG, "authenticate() failed for intent:" + intent, e); 590 } 591 } 592 593 @Override isActivityResumed()594 public boolean isActivityResumed() { 595 return mActivity.isResumed(); 596 } 597 } 598