AutoFillUI.java revision 2aedac13b71fc4f7546bc73adaef32f51b84be68
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 android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_HIDDEN; 19import static android.view.autofill.AutofillManager.AutofillCallback.EVENT_INPUT_SHOWN; 20 21import android.annotation.NonNull; 22import android.annotation.Nullable; 23import android.content.Context; 24import android.content.IntentSender; 25import android.graphics.Rect; 26import android.os.Handler; 27import android.os.IBinder; 28import android.service.autofill.Dataset; 29import android.service.autofill.FillResponse; 30import android.service.autofill.SaveInfo; 31import android.text.TextUtils; 32import android.text.format.DateUtils; 33import android.util.Slog; 34import android.view.autofill.AutofillId; 35import android.widget.Toast; 36 37import com.android.server.UiThread; 38 39import java.io.PrintWriter; 40 41/** 42 * Handles all autofill related UI tasks. The UI has two components: 43 * fill UI that shows a popup style window anchored at the focused 44 * input field for choosing a dataset to fill or trigger the response 45 * authentication flow; save UI that shows a toast style window for 46 * managing saving of user edits. 47 */ 48public final class AutoFillUI { 49 private static final String TAG = "AutoFillUI"; 50 51 private static final int MAX_SAVE_TIMEOUT_MS = (int) (30 * DateUtils.SECOND_IN_MILLIS); 52 53 private final Handler mHandler = UiThread.getHandler(); 54 private final @NonNull Context mContext; 55 56 private @Nullable FillUi mFillUi; 57 private @Nullable SaveUi mSaveUi; 58 59 private @Nullable AutoFillUiCallback mCallback; 60 private @Nullable IBinder mWindowToken; 61 62 private int mSaveTimeoutMs = (int) (5 * DateUtils.SECOND_IN_MILLIS); 63 64 public interface AutoFillUiCallback { 65 void authenticate(@NonNull IntentSender intent); 66 void fill(@NonNull Dataset dataset); 67 void save(); 68 void cancelSave(); 69 void onEvent(AutofillId id, int event); 70 } 71 72 public AutoFillUI(@NonNull Context context) { 73 mContext = context; 74 } 75 76 public void setCallback(@Nullable AutoFillUiCallback callback, 77 @Nullable IBinder windowToken) { 78 mHandler.post(() -> { 79 if (mCallback != callback || mWindowToken != windowToken) { 80 hideAllUiThread(); 81 mCallback = callback; 82 mWindowToken = windowToken; 83 } 84 }); 85 } 86 87 /** 88 * Displays an error message to the user. 89 */ 90 public void showError(@Nullable CharSequence message) { 91 mHandler.post(() -> { 92 if (!hasCallback()) { 93 return; 94 } 95 hideAllUiThread(); 96 if (!TextUtils.isEmpty(message)) { 97 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 98 } 99 }); 100 } 101 102 /** 103 * Hides the fill UI. 104 */ 105 public void hideFillUi(AutofillId id) { 106 mHandler.post(() -> { 107 hideFillUiUiThread(); 108 if (mCallback != null) { 109 mCallback.onEvent(id, EVENT_INPUT_HIDDEN); 110 } 111 }); 112 } 113 114 /** 115 * Filters the options in the fill UI. 116 * 117 * @param filterText The filter prefix. 118 */ 119 public void filterFillUi(@Nullable String filterText) { 120 mHandler.post(() -> { 121 if (!hasCallback()) { 122 return; 123 } 124 hideSaveUiUiThread(); 125 if (mFillUi != null) { 126 mFillUi.setFilterText(filterText); 127 } 128 }); 129 } 130 131 /** 132 * Updates the position of the fill UI. 133 * 134 * @param anchoredBounds The bounds of the anchor view. 135 */ 136 public void updateFillUi(@NonNull Rect anchoredBounds) { 137 mHandler.post(() -> { 138 if (!hasCallback()) { 139 return; 140 } 141 hideSaveUiUiThread(); 142 if (mFillUi != null) { 143 mFillUi.update(anchoredBounds); 144 } 145 }); 146 } 147 148 /** 149 * Shows the fill UI, removing the previous fill UI if the has changed. 150 * 151 * @param focusedId the currently focused field 152 * @param response the current fill response 153 * @param anchorBounds bounds of the focused view 154 * @param filterText text of the view to be filled 155 */ 156 public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response, 157 @NonNull Rect anchorBounds, @Nullable String filterText) { 158 mHandler.post(() -> { 159 if (!hasCallback()) { 160 return; 161 } 162 hideAllUiThread(); 163 mFillUi = new FillUi(mContext, response, focusedId, 164 mWindowToken, anchorBounds, filterText, new FillUi.Callback() { 165 @Override 166 public void onResponsePicked(FillResponse response) { 167 hideFillUiUiThread(); 168 if (mCallback != null) { 169 mCallback.authenticate(response.getAuthentication()); 170 } 171 } 172 173 @Override 174 public void onDatasetPicked(Dataset dataset) { 175 hideFillUiUiThread(); 176 if (mCallback != null) { 177 mCallback.fill(dataset); 178 } 179 // TODO(b/33197203): add MetricsLogger call 180 } 181 182 @Override 183 public void onCanceled() { 184 hideFillUiUiThread(); 185 // TODO(b/33197203): add MetricsLogger call 186 } 187 }); 188 mCallback.onEvent(focusedId, EVENT_INPUT_SHOWN); 189 }); 190 } 191 192 /** 193 * Shows the UI asking the user to save for autofill. 194 */ 195 public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info) { 196 mHandler.post(() -> { 197 if (!hasCallback()) { 198 return; 199 } 200 hideAllUiThread(); 201 mSaveUi = new SaveUi(mContext, providerLabel, info, 202 new SaveUi.OnSaveListener() { 203 @Override 204 public void onSave() { 205 hideSaveUiUiThread(); 206 if (mCallback != null) { 207 mCallback.save(); 208 } 209 // TODO(b/33197203): add MetricsLogger call 210 } 211 212 @Override 213 public void onCancel(IntentSender listener) { 214 // TODO(b/33197203): add MetricsLogger call 215 hideSaveUiUiThread(); 216 if (listener != null) { 217 try { 218 listener.sendIntent(mContext, 0, null, null, null); 219 } catch (IntentSender.SendIntentException e) { 220 Slog.e(TAG, "Error starting negative action listener: " 221 + listener, e); 222 } 223 } 224 if (mCallback != null) { 225 mCallback.cancelSave(); 226 } 227 } 228 }, mSaveTimeoutMs); 229 }); 230 } 231 232 /** 233 * Hides all UI affordances. 234 */ 235 public void hideAll() { 236 mHandler.post(this::hideAllUiThread); 237 } 238 239 public void setSaveTimeout(int timeout) { 240 if (timeout > MAX_SAVE_TIMEOUT_MS) { 241 throw new IllegalArgumentException("Maximum value is " + MAX_SAVE_TIMEOUT_MS + "ms"); 242 } 243 if (timeout <= 0) { 244 throw new IllegalArgumentException("Must be a positive value"); 245 } 246 mSaveTimeoutMs = timeout; 247 } 248 249 public void dump(PrintWriter pw) { 250 pw.println("AufoFill UI"); 251 final String prefix = " "; 252 pw.print(prefix); pw.print("showsFillUi: "); pw.println(mFillUi != null); 253 pw.print(prefix); pw.print("showsSaveUi: "); pw.println(mSaveUi != null); 254 pw.print(prefix); pw.print("save timeout: "); pw.println(mSaveTimeoutMs); 255 } 256 257 @android.annotation.UiThread 258 private void hideFillUiUiThread() { 259 if (mFillUi != null) { 260 mFillUi.destroy(); 261 mFillUi = null; 262 } 263 } 264 265 @android.annotation.UiThread 266 private void hideSaveUiUiThread() { 267 if (mSaveUi != null) { 268 mSaveUi.destroy(); 269 mSaveUi = null; 270 } 271 } 272 273 @android.annotation.UiThread 274 private void hideAllUiThread() { 275 hideFillUiUiThread(); 276 hideSaveUiUiThread(); 277 } 278 279 private boolean hasCallback() { 280 return mCallback != null; 281 } 282} 283