1 /* 2 * Copyright (C) 2016 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 package com.android.server.autofill.ui; 17 18 import static com.android.server.autofill.Helper.sDebug; 19 import static com.android.server.autofill.Helper.sVerbose; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentSender; 27 import android.graphics.drawable.Drawable; 28 import android.metrics.LogMaker; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.IBinder; 32 import android.os.RemoteException; 33 import android.service.autofill.Dataset; 34 import android.service.autofill.FillResponse; 35 import android.service.autofill.SaveInfo; 36 import android.service.autofill.ValueFinder; 37 import android.text.TextUtils; 38 import android.util.Slog; 39 import android.view.KeyEvent; 40 import android.view.autofill.AutofillId; 41 import android.view.autofill.AutofillManager; 42 import android.view.autofill.IAutofillWindowPresenter; 43 import android.widget.Toast; 44 45 import com.android.internal.logging.MetricsLogger; 46 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 47 import com.android.server.LocalServices; 48 import com.android.server.UiModeManagerInternal; 49 import com.android.server.UiThread; 50 import com.android.server.autofill.Helper; 51 52 import java.io.PrintWriter; 53 54 /** 55 * Handles all autofill related UI tasks. The UI has two components: 56 * fill UI that shows a popup style window anchored at the focused 57 * input field for choosing a dataset to fill or trigger the response 58 * authentication flow; save UI that shows a toast style window for 59 * managing saving of user edits. 60 */ 61 public final class AutoFillUI { 62 private static final String TAG = "AutofillUI"; 63 64 private final Handler mHandler = UiThread.getHandler(); 65 private final @NonNull Context mContext; 66 67 private @Nullable FillUi mFillUi; 68 private @Nullable SaveUi mSaveUi; 69 70 private @Nullable AutoFillUiCallback mCallback; 71 72 private final MetricsLogger mMetricsLogger = new MetricsLogger(); 73 74 private final @NonNull OverlayControl mOverlayControl; 75 private final @NonNull UiModeManagerInternal mUiModeMgr; 76 77 private @Nullable Runnable mCreateFillUiRunnable; 78 private @Nullable AutoFillUiCallback mSaveUiCallback; 79 80 public interface AutoFillUiCallback { authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent, @Nullable Bundle extras, boolean authenticateInline)81 void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent, 82 @Nullable Bundle extras, boolean authenticateInline); fill(int requestId, int datasetIndex, @NonNull Dataset dataset)83 void fill(int requestId, int datasetIndex, @NonNull Dataset dataset); save()84 void save(); cancelSave()85 void cancelSave(); requestShowFillUi(AutofillId id, int width, int height, IAutofillWindowPresenter presenter)86 void requestShowFillUi(AutofillId id, int width, int height, 87 IAutofillWindowPresenter presenter); requestHideFillUi(AutofillId id)88 void requestHideFillUi(AutofillId id); startIntentSenderAndFinishSession(IntentSender intentSender)89 void startIntentSenderAndFinishSession(IntentSender intentSender); startIntentSender(IntentSender intentSender, Intent intent)90 void startIntentSender(IntentSender intentSender, Intent intent); dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent)91 void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent); cancelSession()92 void cancelSession(); 93 } 94 AutoFillUI(@onNull Context context)95 public AutoFillUI(@NonNull Context context) { 96 mContext = context; 97 mOverlayControl = new OverlayControl(context); 98 mUiModeMgr = LocalServices.getService(UiModeManagerInternal.class); 99 } 100 setCallback(@onNull AutoFillUiCallback callback)101 public void setCallback(@NonNull AutoFillUiCallback callback) { 102 mHandler.post(() -> { 103 if (mCallback != callback) { 104 if (mCallback != null) { 105 if (isSaveUiShowing()) { 106 // keeps showing the save UI 107 hideFillUiUiThread(callback, true); 108 } else { 109 hideAllUiThread(mCallback); 110 } 111 } 112 mCallback = callback; 113 } 114 }); 115 } 116 clearCallback(@onNull AutoFillUiCallback callback)117 public void clearCallback(@NonNull AutoFillUiCallback callback) { 118 mHandler.post(() -> { 119 if (mCallback == callback) { 120 hideAllUiThread(callback); 121 mCallback = null; 122 } 123 }); 124 } 125 126 /** 127 * Displays an error message to the user. 128 */ showError(int resId, @NonNull AutoFillUiCallback callback)129 public void showError(int resId, @NonNull AutoFillUiCallback callback) { 130 showError(mContext.getString(resId), callback); 131 } 132 133 /** 134 * Displays an error message to the user. 135 */ showError(@ullable CharSequence message, @NonNull AutoFillUiCallback callback)136 public void showError(@Nullable CharSequence message, @NonNull AutoFillUiCallback callback) { 137 Slog.w(TAG, "showError(): " + message); 138 139 mHandler.post(() -> { 140 if (mCallback != callback) { 141 return; 142 } 143 hideAllUiThread(callback); 144 if (!TextUtils.isEmpty(message)) { 145 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 146 } 147 }); 148 } 149 150 /** 151 * Hides the fill UI. 152 */ hideFillUi(@onNull AutoFillUiCallback callback)153 public void hideFillUi(@NonNull AutoFillUiCallback callback) { 154 mHandler.post(() -> hideFillUiUiThread(callback, true)); 155 } 156 157 /** 158 * Filters the options in the fill UI. 159 * 160 * @param filterText The filter prefix. 161 */ filterFillUi(@ullable String filterText, @NonNull AutoFillUiCallback callback)162 public void filterFillUi(@Nullable String filterText, @NonNull AutoFillUiCallback callback) { 163 mHandler.post(() -> { 164 if (callback != mCallback) { 165 return; 166 } 167 if (mFillUi != null) { 168 mFillUi.setFilterText(filterText); 169 } 170 }); 171 } 172 173 /** 174 * Shows the fill UI, removing the previous fill UI if the has changed. 175 * 176 * @param focusedId the currently focused field 177 * @param response the current fill response 178 * @param filterText text of the view to be filled 179 * @param servicePackageName package name of the autofill service filling the activity 180 * @param componentName component name of the activity that is filled 181 * @param serviceLabel label of autofill service 182 * @param serviceIcon icon of autofill service 183 * @param callback identifier for the caller 184 * @param sessionId id of the autofill session 185 * @param compatMode whether the app is being autofilled in compatibility mode. 186 */ showFillUi(@onNull AutofillId focusedId, @NonNull FillResponse response, @Nullable String filterText, @Nullable String servicePackageName, @NonNull ComponentName componentName, @NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback, int sessionId, boolean compatMode)187 public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response, 188 @Nullable String filterText, @Nullable String servicePackageName, 189 @NonNull ComponentName componentName, @NonNull CharSequence serviceLabel, 190 @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback, int sessionId, 191 boolean compatMode) { 192 if (sDebug) { 193 final int size = filterText == null ? 0 : filterText.length(); 194 Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars"); 195 } 196 final LogMaker log = Helper 197 .newLogMaker(MetricsEvent.AUTOFILL_FILL_UI, componentName, servicePackageName, 198 sessionId, compatMode) 199 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FILTERTEXT_LEN, 200 filterText == null ? 0 : filterText.length()) 201 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, 202 response.getDatasets() == null ? 0 : response.getDatasets().size()); 203 204 final Runnable createFillUiRunnable = () -> { 205 if (callback != mCallback) { 206 return; 207 } 208 hideAllUiThread(callback); 209 mFillUi = new FillUi(mContext, response, focusedId, 210 filterText, mOverlayControl, serviceLabel, serviceIcon, 211 mUiModeMgr.isNightMode(), 212 new FillUi.Callback() { 213 @Override 214 public void onResponsePicked(FillResponse response) { 215 log.setType(MetricsEvent.TYPE_DETAIL); 216 hideFillUiUiThread(callback, true); 217 if (mCallback != null) { 218 mCallback.authenticate(response.getRequestId(), 219 AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED, 220 response.getAuthentication(), response.getClientState(), 221 /* authenticateInline= */ false); 222 } 223 } 224 225 @Override 226 public void onDatasetPicked(Dataset dataset) { 227 log.setType(MetricsEvent.TYPE_ACTION); 228 hideFillUiUiThread(callback, true); 229 if (mCallback != null) { 230 final int datasetIndex = response.getDatasets().indexOf(dataset); 231 mCallback.fill(response.getRequestId(), datasetIndex, dataset); 232 } 233 } 234 235 @Override 236 public void onCanceled() { 237 log.setType(MetricsEvent.TYPE_DISMISS); 238 hideFillUiUiThread(callback, true); 239 } 240 241 @Override 242 public void onDestroy() { 243 if (log.getType() == MetricsEvent.TYPE_UNKNOWN) { 244 log.setType(MetricsEvent.TYPE_CLOSE); 245 } 246 mMetricsLogger.write(log); 247 } 248 249 @Override 250 public void requestShowFillUi(int width, int height, 251 IAutofillWindowPresenter windowPresenter) { 252 if (mCallback != null) { 253 mCallback.requestShowFillUi(focusedId, width, height, windowPresenter); 254 } 255 } 256 257 @Override 258 public void requestHideFillUi() { 259 if (mCallback != null) { 260 mCallback.requestHideFillUi(focusedId); 261 } 262 } 263 264 @Override 265 public void startIntentSender(IntentSender intentSender) { 266 if (mCallback != null) { 267 mCallback.startIntentSenderAndFinishSession(intentSender); 268 } 269 } 270 271 @Override 272 public void dispatchUnhandledKey(KeyEvent keyEvent) { 273 if (mCallback != null) { 274 mCallback.dispatchUnhandledKey(focusedId, keyEvent); 275 } 276 } 277 278 @Override 279 public void cancelSession() { 280 if (mCallback != null) { 281 mCallback.cancelSession(); 282 } 283 } 284 }); 285 }; 286 287 if (isSaveUiShowing()) { 288 // postpone creating the fill UI for showing the save UI 289 if (sDebug) Slog.d(TAG, "postpone fill UI request.."); 290 mCreateFillUiRunnable = createFillUiRunnable; 291 } else { 292 mHandler.post(createFillUiRunnable); 293 } 294 } 295 296 /** 297 * Shows the UI asking the user to save for autofill. 298 */ showSaveUi(@onNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, @Nullable String servicePackageName, @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, @NonNull ComponentName componentName, @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingSaveUi, boolean isUpdate, boolean compatMode)299 public void showSaveUi(@NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, 300 @Nullable String servicePackageName, @NonNull SaveInfo info, 301 @NonNull ValueFinder valueFinder, @NonNull ComponentName componentName, 302 @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingSaveUi, 303 boolean isUpdate, boolean compatMode) { 304 if (sVerbose) { 305 Slog.v(TAG, "showSaveUi(update=" + isUpdate + ") for " + componentName.toShortString() 306 + ": " + info); 307 } 308 int numIds = 0; 309 numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length; 310 numIds += info.getOptionalIds() == null ? 0 : info.getOptionalIds().length; 311 312 final LogMaker log = Helper 313 .newLogMaker(MetricsEvent.AUTOFILL_SAVE_UI, componentName, servicePackageName, 314 pendingSaveUi.sessionId, compatMode) 315 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_IDS, numIds); 316 if (isUpdate) { 317 log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_UPDATE, 1); 318 } 319 320 mHandler.post(() -> { 321 if (callback != mCallback) { 322 return; 323 } 324 hideAllUiThread(callback); 325 mSaveUiCallback = callback; 326 mSaveUi = new SaveUi(mContext, pendingSaveUi, serviceLabel, serviceIcon, 327 servicePackageName, componentName, info, valueFinder, mOverlayControl, 328 new SaveUi.OnSaveListener() { 329 @Override 330 public void onSave() { 331 log.setType(MetricsEvent.TYPE_ACTION); 332 hideSaveUiUiThread(callback); 333 callback.save(); 334 destroySaveUiUiThread(pendingSaveUi, true); 335 } 336 337 @Override 338 public void onCancel(IntentSender listener) { 339 log.setType(MetricsEvent.TYPE_DISMISS); 340 hideSaveUiUiThread(callback); 341 if (listener != null) { 342 try { 343 listener.sendIntent(mContext, 0, null, null, null); 344 } catch (IntentSender.SendIntentException e) { 345 Slog.e(TAG, "Error starting negative action listener: " 346 + listener, e); 347 } 348 } 349 callback.cancelSave(); 350 destroySaveUiUiThread(pendingSaveUi, true); 351 } 352 353 @Override 354 public void onDestroy() { 355 if (log.getType() == MetricsEvent.TYPE_UNKNOWN) { 356 log.setType(MetricsEvent.TYPE_CLOSE); 357 358 callback.cancelSave(); 359 } 360 mMetricsLogger.write(log); 361 } 362 363 @Override 364 public void startIntentSender(IntentSender intentSender, Intent intent) { 365 callback.startIntentSender(intentSender, intent); 366 } 367 }, mUiModeMgr.isNightMode(), isUpdate, compatMode); 368 }); 369 } 370 371 /** 372 * Executes an operation in the pending save UI, if any. 373 */ onPendingSaveUi(int operation, @NonNull IBinder token)374 public void onPendingSaveUi(int operation, @NonNull IBinder token) { 375 mHandler.post(() -> { 376 if (mSaveUi != null) { 377 mSaveUi.onPendingUi(operation, token); 378 } else { 379 Slog.w(TAG, "onPendingSaveUi(" + operation + "): no save ui"); 380 } 381 }); 382 } 383 384 /** 385 * Hides all autofill UIs. 386 */ hideAll(@ullable AutoFillUiCallback callback)387 public void hideAll(@Nullable AutoFillUiCallback callback) { 388 mHandler.post(() -> hideAllUiThread(callback)); 389 } 390 391 /** 392 * Destroy all autofill UIs. 393 */ destroyAll(@ullable PendingUi pendingSaveUi, @Nullable AutoFillUiCallback callback, boolean notifyClient)394 public void destroyAll(@Nullable PendingUi pendingSaveUi, 395 @Nullable AutoFillUiCallback callback, boolean notifyClient) { 396 mHandler.post(() -> destroyAllUiThread(pendingSaveUi, callback, notifyClient)); 397 } 398 isSaveUiShowing()399 public boolean isSaveUiShowing() { 400 return mSaveUi == null ? false : mSaveUi.isShowing(); 401 } 402 dump(PrintWriter pw)403 public void dump(PrintWriter pw) { 404 pw.println("Autofill UI"); 405 final String prefix = " "; 406 final String prefix2 = " "; 407 pw.print(prefix); pw.print("Night mode: "); pw.println(mUiModeMgr.isNightMode()); 408 if (mFillUi != null) { 409 pw.print(prefix); pw.println("showsFillUi: true"); 410 mFillUi.dump(pw, prefix2); 411 } else { 412 pw.print(prefix); pw.println("showsFillUi: false"); 413 } 414 if (mSaveUi != null) { 415 pw.print(prefix); pw.println("showsSaveUi: true"); 416 mSaveUi.dump(pw, prefix2); 417 } else { 418 pw.print(prefix); pw.println("showsSaveUi: false"); 419 } 420 } 421 422 @android.annotation.UiThread hideFillUiUiThread(@ullable AutoFillUiCallback callback, boolean notifyClient)423 private void hideFillUiUiThread(@Nullable AutoFillUiCallback callback, boolean notifyClient) { 424 if (mFillUi != null && (callback == null || callback == mCallback)) { 425 mFillUi.destroy(notifyClient); 426 mFillUi = null; 427 } 428 } 429 430 @android.annotation.UiThread 431 @Nullable hideSaveUiUiThread(@ullable AutoFillUiCallback callback)432 private PendingUi hideSaveUiUiThread(@Nullable AutoFillUiCallback callback) { 433 if (sVerbose) { 434 Slog.v(TAG, "hideSaveUiUiThread(): mSaveUi=" + mSaveUi + ", callback=" + callback 435 + ", mCallback=" + mCallback); 436 } 437 438 if (mSaveUi != null && mSaveUiCallback == callback) { 439 return mSaveUi.hide(); 440 } 441 return null; 442 } 443 444 @android.annotation.UiThread destroySaveUiUiThread(@ullable PendingUi pendingSaveUi, boolean notifyClient)445 private void destroySaveUiUiThread(@Nullable PendingUi pendingSaveUi, boolean notifyClient) { 446 if (mSaveUi == null) { 447 // Calling destroySaveUiUiThread() twice is normal - it usually happens when the 448 // first call is made after the SaveUI is hidden and the second when the session is 449 // finished. 450 if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): already destroyed"); 451 return; 452 } 453 454 if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): " + pendingSaveUi); 455 mSaveUi.destroy(); 456 mSaveUi = null; 457 mSaveUiCallback = null; 458 if (pendingSaveUi != null && notifyClient) { 459 try { 460 if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): notifying client"); 461 pendingSaveUi.client.setSaveUiState(pendingSaveUi.sessionId, false); 462 } catch (RemoteException e) { 463 Slog.e(TAG, "Error notifying client to set save UI state to hidden: " + e); 464 } 465 } 466 467 if (mCreateFillUiRunnable != null) { 468 if (sDebug) Slog.d(TAG, "start the pending fill UI request.."); 469 mHandler.post(mCreateFillUiRunnable); 470 mCreateFillUiRunnable = null; 471 } 472 } 473 474 @android.annotation.UiThread destroyAllUiThread(@ullable PendingUi pendingSaveUi, @Nullable AutoFillUiCallback callback, boolean notifyClient)475 private void destroyAllUiThread(@Nullable PendingUi pendingSaveUi, 476 @Nullable AutoFillUiCallback callback, boolean notifyClient) { 477 hideFillUiUiThread(callback, notifyClient); 478 destroySaveUiUiThread(pendingSaveUi, notifyClient); 479 } 480 481 @android.annotation.UiThread hideAllUiThread(@ullable AutoFillUiCallback callback)482 private void hideAllUiThread(@Nullable AutoFillUiCallback callback) { 483 hideFillUiUiThread(callback, true); 484 final PendingUi pendingSaveUi = hideSaveUiUiThread(callback); 485 if (pendingSaveUi != null && pendingSaveUi.getState() == PendingUi.STATE_FINISHED) { 486 if (sDebug) { 487 Slog.d(TAG, "hideAllUiThread(): " 488 + "destroying Save UI because pending restoration is finished"); 489 } 490 destroySaveUiUiThread(pendingSaveUi, true); 491 } 492 } 493 } 494