SaveUi.java revision 838ba3dbbd0753b3bbf7cff6232e57c488cfdf2d
1/* 2 * Copyright (C) 2017 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 17package com.android.server.autofill.ui; 18 19import static com.android.server.autofill.Helper.sDebug; 20import static com.android.server.autofill.Helper.sVerbose; 21 22import android.annotation.NonNull; 23import android.app.Dialog; 24import android.app.PendingIntent; 25import android.app.PendingIntent.CanceledException; 26import android.content.Context; 27import android.content.Intent; 28import android.content.IntentSender; 29import android.os.Handler; 30import android.os.IBinder; 31import android.os.RemoteException; 32import android.service.autofill.CustomDescription; 33import android.service.autofill.SaveInfo; 34import android.service.autofill.ValueFinder; 35import android.text.Html; 36import android.util.ArraySet; 37import android.util.Slog; 38import android.view.Gravity; 39import android.view.LayoutInflater; 40import android.view.View; 41import android.view.ViewGroup; 42import android.view.ViewGroup.LayoutParams; 43import android.view.Window; 44import android.view.WindowManager; 45import android.view.autofill.AutofillManager; 46import android.view.autofill.IAutoFillManagerClient; 47import android.widget.RemoteViews; 48import android.widget.ScrollView; 49import android.widget.TextView; 50 51import com.android.internal.R; 52import com.android.server.UiThread; 53 54import java.io.PrintWriter; 55 56/** 57 * Autofill Save Prompt 58 */ 59final class SaveUi { 60 61 private static final String TAG = "AutofillSaveUi"; 62 63 public interface OnSaveListener { 64 void onSave(); 65 void onCancel(IntentSender listener); 66 void onDestroy(); 67 } 68 69 private class OneTimeListener implements OnSaveListener { 70 71 private final OnSaveListener mRealListener; 72 private boolean mDone; 73 74 OneTimeListener(OnSaveListener realListener) { 75 mRealListener = realListener; 76 } 77 78 @Override 79 public void onSave() { 80 if (sDebug) Slog.d(TAG, "OneTimeListener.onSave(): " + mDone); 81 if (mDone) { 82 return; 83 } 84 mDone = true; 85 mRealListener.onSave(); 86 } 87 88 @Override 89 public void onCancel(IntentSender listener) { 90 if (sDebug) Slog.d(TAG, "OneTimeListener.onCancel(): " + mDone); 91 if (mDone) { 92 return; 93 } 94 mDone = true; 95 mRealListener.onCancel(listener); 96 } 97 98 @Override 99 public void onDestroy() { 100 if (sDebug) Slog.d(TAG, "OneTimeListener.onDestroy(): " + mDone); 101 if (mDone) { 102 return; 103 } 104 mDone = true; 105 mRealListener.onDestroy(); 106 } 107 } 108 109 private final Handler mHandler = UiThread.getHandler(); 110 111 private final @NonNull Dialog mDialog; 112 113 private final @NonNull OneTimeListener mListener; 114 115 private final @NonNull OverlayControl mOverlayControl; 116 117 private final CharSequence mTitle; 118 private final CharSequence mSubTitle; 119 private final PendingUi mPendingUi; 120 121 private boolean mDestroyed; 122 123 SaveUi(@NonNull Context context, @NonNull PendingUi pendingUi, 124 @NonNull CharSequence providerLabel, @NonNull SaveInfo info, 125 @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl, 126 @NonNull OnSaveListener listener) { 127 mPendingUi= pendingUi; 128 mListener = new OneTimeListener(listener); 129 mOverlayControl = overlayControl; 130 131 final LayoutInflater inflater = LayoutInflater.from(context); 132 final View view = inflater.inflate(R.layout.autofill_save, null); 133 134 final TextView titleView = view.findViewById(R.id.autofill_save_title); 135 136 final ArraySet<String> types = new ArraySet<>(3); 137 final int type = info.getType(); 138 139 if ((type & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) { 140 types.add(context.getString(R.string.autofill_save_type_password)); 141 } 142 if ((type & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) { 143 types.add(context.getString(R.string.autofill_save_type_address)); 144 } 145 if ((type & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) { 146 types.add(context.getString(R.string.autofill_save_type_credit_card)); 147 } 148 if ((type & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) { 149 types.add(context.getString(R.string.autofill_save_type_username)); 150 } 151 if ((type & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) { 152 types.add(context.getString(R.string.autofill_save_type_email_address)); 153 } 154 155 switch (types.size()) { 156 case 1: 157 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type, 158 types.valueAt(0), providerLabel), 0); 159 break; 160 case 2: 161 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_2types, 162 types.valueAt(0), types.valueAt(1), providerLabel), 0); 163 break; 164 case 3: 165 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_3types, 166 types.valueAt(0), types.valueAt(1), types.valueAt(2), providerLabel), 0); 167 break; 168 default: 169 // Use generic if more than 3 or invalid type (size 0). 170 mTitle = Html.fromHtml( 171 context.getString(R.string.autofill_save_title, providerLabel), 0); 172 } 173 174 titleView.setText(mTitle); 175 176 ScrollView subtitleContainer = null; 177 final CustomDescription customDescription = info.getCustomDescription(); 178 if (customDescription != null) { 179 mSubTitle = null; 180 if (sDebug) Slog.d(TAG, "Using custom description"); 181 182 final RemoteViews presentation = customDescription.getPresentation(valueFinder); 183 if (presentation != null) { 184 final RemoteViews.OnClickHandler handler = new RemoteViews.OnClickHandler() { 185 @Override 186 public boolean onClickHandler(View view, PendingIntent pendingIntent, 187 Intent intent) { 188 // We need to hide the Save UI before launching the pending intent, and 189 // restore back it once the activity is finished, and that's achieved by 190 // adding a custom extra in the activity intent. 191 if (pendingIntent != null) { 192 if (intent == null) { 193 Slog.w(TAG, 194 "remote view on custom description does not have intent"); 195 return false; 196 } 197 if (!pendingIntent.isActivity()) { 198 Slog.w(TAG, "ignoring custom description pending intent that's not " 199 + "for an activity: " + pendingIntent); 200 return false; 201 } 202 if (sVerbose) { 203 Slog.v(TAG, 204 "Intercepting custom description intent: " + intent); 205 } 206 final IBinder token = mPendingUi.getToken(); 207 intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); 208 try { 209 pendingUi.client.startIntentSender(pendingIntent.getIntentSender(), 210 intent); 211 mPendingUi.setState(PendingUi.STATE_PENDING); 212 if (sDebug) { 213 Slog.d(TAG, "hiding UI until restored with token " + token); 214 } 215 hide(); 216 } catch (RemoteException e) { 217 Slog.w(TAG, "error triggering pending intent: " + intent); 218 return false; 219 } 220 } 221 return true; 222 } 223 }; 224 225 try { 226 final View customSubtitleView = presentation.apply(context, null, handler); 227 subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle); 228 subtitleContainer.addView(customSubtitleView); 229 subtitleContainer.setVisibility(View.VISIBLE); 230 } catch (Exception e) { 231 Slog.e(TAG, "Could not inflate custom description. ", e); 232 } 233 } else { 234 Slog.w(TAG, "could not create remote presentation for custom title"); 235 } 236 } else { 237 mSubTitle = info.getDescription(); 238 if (mSubTitle != null) { 239 subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle); 240 final TextView subtitleView = new TextView(context); 241 subtitleView.setText(mSubTitle); 242 subtitleContainer.addView(subtitleView, 243 new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 244 ViewGroup.LayoutParams.WRAP_CONTENT)); 245 subtitleContainer.setVisibility(View.VISIBLE); 246 } 247 if (sDebug) Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); 248 } 249 250 final TextView noButton = view.findViewById(R.id.autofill_save_no); 251 if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { 252 noButton.setText(R.string.save_password_notnow); 253 } else { 254 noButton.setText(R.string.autofill_save_no); 255 } 256 final View.OnClickListener cancelListener = 257 (v) -> mListener.onCancel(info.getNegativeActionListener()); 258 noButton.setOnClickListener(cancelListener); 259 260 final View yesButton = view.findViewById(R.id.autofill_save_yes); 261 yesButton.setOnClickListener((v) -> mListener.onSave()); 262 263 mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel); 264 mDialog.setContentView(view); 265 266 // Dialog can be dismissed when touched outside. 267 mDialog.setOnDismissListener((d) -> mListener.onCancel(info.getNegativeActionListener())); 268 269 final Window window = mDialog.getWindow(); 270 window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 271 window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 272 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 273 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 274 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 275 window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS); 276 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); 277 window.setGravity(Gravity.BOTTOM | Gravity.CENTER); 278 window.setCloseOnTouchOutside(true); 279 final WindowManager.LayoutParams params = window.getAttributes(); 280 params.width = WindowManager.LayoutParams.MATCH_PARENT; 281 params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); 282 params.windowAnimations = R.style.AutofillSaveAnimation; 283 284 show(); 285 } 286 287 /** 288 * Update the pending UI, if any. 289 * 290 * @param operation how to update it. 291 * @param token token associated with the pending UI - if it doesn't match the pending token, 292 * the operation will be ignored. 293 */ 294 void onPendingUi(int operation, @NonNull IBinder token) { 295 if (!mPendingUi.matches(token)) { 296 Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of " 297 + mPendingUi.getToken()); 298 return; 299 } 300 switch (operation) { 301 case AutofillManager.PENDING_UI_OPERATION_RESTORE: 302 if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token); 303 show(); 304 break; 305 case AutofillManager.PENDING_UI_OPERATION_CANCEL: 306 if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token); 307 hide(); 308 break; 309 default: 310 Slog.w(TAG, "restore(): invalid operation " + operation); 311 } 312 mPendingUi.setState(PendingUi.STATE_FINISHED); 313 } 314 315 private void show() { 316 Slog.i(TAG, "Showing save dialog: " + mTitle); 317 mDialog.show(); 318 mOverlayControl.hideOverlays(); 319 } 320 321 PendingUi hide() { 322 if (sVerbose) Slog.v(TAG, "Hiding save dialog."); 323 try { 324 mDialog.hide(); 325 } finally { 326 mOverlayControl.showOverlays(); 327 } 328 return mPendingUi; 329 } 330 331 void destroy() { 332 try { 333 if (sDebug) Slog.d(TAG, "destroy()"); 334 throwIfDestroyed(); 335 mListener.onDestroy(); 336 mHandler.removeCallbacksAndMessages(mListener); 337 mDialog.dismiss(); 338 mDestroyed = true; 339 } finally { 340 mOverlayControl.showOverlays(); 341 } 342 } 343 344 private void throwIfDestroyed() { 345 if (mDestroyed) { 346 throw new IllegalStateException("cannot interact with a destroyed instance"); 347 } 348 } 349 350 @Override 351 public String toString() { 352 return mTitle == null ? "NO TITLE" : mTitle.toString(); 353 } 354 355 void dump(PrintWriter pw, String prefix) { 356 pw.print(prefix); pw.print("title: "); pw.println(mTitle); 357 pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); 358 pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi); 359 360 final View view = mDialog.getWindow().getDecorView(); 361 final int[] loc = view.getLocationOnScreen(); 362 pw.print(prefix); pw.print("coordinates: "); 363 pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')'); 364 pw.print('('); 365 pw.print(loc[0] + view.getWidth()); pw.print(','); 366 pw.print(loc[1] + view.getHeight());pw.println(')'); 367 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 368 } 369} 370