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