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