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