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