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