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