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 17 package com.android.server.autofill.ui; 18 19 import static com.android.server.autofill.Helper.sDebug; 20 import static com.android.server.autofill.Helper.sVerbose; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.Dialog; 25 import android.app.PendingIntent; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentSender; 30 import android.content.pm.ActivityInfo; 31 import android.content.pm.PackageManager; 32 import android.content.res.Resources; 33 import android.graphics.drawable.Drawable; 34 import android.metrics.LogMaker; 35 import android.os.Handler; 36 import android.os.IBinder; 37 import android.os.UserHandle; 38 import android.service.autofill.BatchUpdates; 39 import android.service.autofill.CustomDescription; 40 import android.service.autofill.InternalOnClickAction; 41 import android.service.autofill.InternalTransformation; 42 import android.service.autofill.InternalValidator; 43 import android.service.autofill.SaveInfo; 44 import android.service.autofill.ValueFinder; 45 import android.text.Html; 46 import android.text.SpannableStringBuilder; 47 import android.text.TextUtils; 48 import android.text.method.LinkMovementMethod; 49 import android.text.style.ClickableSpan; 50 import android.util.ArraySet; 51 import android.util.Pair; 52 import android.util.Slog; 53 import android.util.SparseArray; 54 import android.view.ContextThemeWrapper; 55 import android.view.Gravity; 56 import android.view.LayoutInflater; 57 import android.view.View; 58 import android.view.ViewGroup; 59 import android.view.ViewGroup.LayoutParams; 60 import android.view.Window; 61 import android.view.WindowManager; 62 import android.view.autofill.AutofillManager; 63 import android.widget.ImageView; 64 import android.widget.RemoteViews; 65 import android.widget.TextView; 66 67 import com.android.internal.R; 68 import com.android.internal.logging.MetricsLogger; 69 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 70 import com.android.internal.util.ArrayUtils; 71 import com.android.server.UiThread; 72 import com.android.server.autofill.Helper; 73 import com.android.server.utils.Slogf; 74 75 import java.io.PrintWriter; 76 import java.util.ArrayList; 77 import java.util.List; 78 import java.util.function.Predicate; 79 80 /** 81 * Autofill Save Prompt 82 */ 83 final class SaveUi { 84 85 private static final String TAG = "SaveUi"; 86 87 private static final int THEME_ID_LIGHT = 88 com.android.internal.R.style.Theme_DeviceDefault_Light_Autofill_Save; 89 private static final int THEME_ID_DARK = 90 com.android.internal.R.style.Theme_DeviceDefault_Autofill_Save; 91 92 private static final int SCROLL_BAR_DEFAULT_DELAY_BEFORE_FADE_MS = 500; 93 94 public interface OnSaveListener { onSave()95 void onSave(); onCancel(IntentSender listener)96 void onCancel(IntentSender listener); onDestroy()97 void onDestroy(); startIntentSender(IntentSender intentSender, Intent intent)98 void startIntentSender(IntentSender intentSender, Intent intent); 99 } 100 101 /** 102 * Wrapper that guarantees that only one callback action (either {@link #onSave()} or 103 * {@link #onCancel(IntentSender)}) is triggered by ignoring further calls after 104 * it's destroyed. 105 * 106 * <p>It's needed becase {@link #onCancel(IntentSender)} is always called when the Save UI 107 * dialog is dismissed. 108 */ 109 private class OneActionThenDestroyListener implements OnSaveListener { 110 111 private final OnSaveListener mRealListener; 112 private boolean mDone; 113 OneActionThenDestroyListener(OnSaveListener realListener)114 OneActionThenDestroyListener(OnSaveListener realListener) { 115 mRealListener = realListener; 116 } 117 118 @Override onSave()119 public void onSave() { 120 if (sDebug) Slog.d(TAG, "OneTimeListener.onSave(): " + mDone); 121 if (mDone) { 122 return; 123 } 124 mRealListener.onSave(); 125 } 126 127 @Override onCancel(IntentSender listener)128 public void onCancel(IntentSender listener) { 129 if (sDebug) Slog.d(TAG, "OneTimeListener.onCancel(): " + mDone); 130 if (mDone) { 131 return; 132 } 133 mRealListener.onCancel(listener); 134 } 135 136 @Override onDestroy()137 public void onDestroy() { 138 if (sDebug) Slog.d(TAG, "OneTimeListener.onDestroy(): " + mDone); 139 if (mDone) { 140 return; 141 } 142 mDone = true; 143 mRealListener.onDestroy(); 144 } 145 146 @Override startIntentSender(IntentSender intentSender, Intent intent)147 public void startIntentSender(IntentSender intentSender, Intent intent) { 148 if (sDebug) Slog.d(TAG, "OneTimeListener.startIntentSender(): " + mDone); 149 if (mDone) { 150 return; 151 } 152 mRealListener.startIntentSender(intentSender, intent); 153 } 154 } 155 156 private final Handler mHandler = UiThread.getHandler(); 157 private final MetricsLogger mMetricsLogger = new MetricsLogger(); 158 159 private final @NonNull Dialog mDialog; 160 161 private final @NonNull OneActionThenDestroyListener mListener; 162 163 private final @NonNull OverlayControl mOverlayControl; 164 165 private final CharSequence mTitle; 166 private final CharSequence mSubTitle; 167 private final PendingUi mPendingUi; 168 private final String mServicePackageName; 169 private final ComponentName mComponentName; 170 private final boolean mCompatMode; 171 private final int mThemeId; 172 private final int mType; 173 174 private boolean mDestroyed; 175 SaveUi(@onNull Context context, @NonNull PendingUi pendingUi, @NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, @Nullable String servicePackageName, @NonNull ComponentName componentName, @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener, boolean nightMode, boolean isUpdate, boolean compatMode, boolean showServiceIcon)176 SaveUi(@NonNull Context context, @NonNull PendingUi pendingUi, 177 @NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon, 178 @Nullable String servicePackageName, @NonNull ComponentName componentName, 179 @NonNull SaveInfo info, @NonNull ValueFinder valueFinder, 180 @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener, 181 boolean nightMode, boolean isUpdate, boolean compatMode, boolean showServiceIcon) { 182 if (sVerbose) { 183 Slogf.v(TAG, "nightMode: %b displayId: %d", nightMode, context.getDisplayId()); 184 } 185 mThemeId = nightMode ? THEME_ID_DARK : THEME_ID_LIGHT; 186 mPendingUi = pendingUi; 187 mListener = new OneActionThenDestroyListener(listener); 188 mOverlayControl = overlayControl; 189 mServicePackageName = servicePackageName; 190 mComponentName = componentName; 191 mCompatMode = compatMode; 192 193 context = new ContextThemeWrapper(context, mThemeId) { 194 @Override 195 public void startActivity(Intent intent) { 196 if (resolveActivity(intent) == null) { 197 if (sDebug) { 198 Slog.d(TAG, "Can not startActivity for save UI with intent=" + intent); 199 } 200 return; 201 } 202 intent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true); 203 204 PendingIntent p = PendingIntent.getActivityAsUser(this, /* requestCode= */ 0, 205 intent, 206 PendingIntent.FLAG_MUTABLE 207 | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, 208 /* options= */ null, UserHandle.CURRENT); 209 if (sDebug) { 210 Slog.d(TAG, "startActivity add save UI restored with intent=" + intent); 211 } 212 // Apply restore mechanism 213 startIntentSenderWithRestore(p, intent); 214 } 215 216 private ComponentName resolveActivity(Intent intent) { 217 final PackageManager packageManager = getPackageManager(); 218 final ComponentName componentName = intent.resolveActivity(packageManager); 219 if (componentName != null) { 220 return componentName; 221 } 222 intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL); 223 final ActivityInfo ai = 224 intent.resolveActivityInfo(packageManager, PackageManager.MATCH_INSTANT); 225 if (ai != null) { 226 return new ComponentName(ai.applicationInfo.packageName, ai.name); 227 } 228 229 return null; 230 } 231 }; 232 final LayoutInflater inflater = LayoutInflater.from(context); 233 final View view = inflater.inflate(R.layout.autofill_save, null); 234 235 final TextView titleView = view.findViewById(R.id.autofill_save_title); 236 237 final ArraySet<String> types = new ArraySet<>(3); 238 mType = info.getType(); 239 240 if ((mType & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) { 241 types.add(context.getString(R.string.autofill_save_type_password)); 242 } 243 if ((mType & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) { 244 types.add(context.getString(R.string.autofill_save_type_address)); 245 } 246 247 // fallback to generic card type if set multiple types 248 final int cardTypeMask = SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD 249 | SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD 250 | SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD; 251 final int count = Integer.bitCount(mType & cardTypeMask); 252 if (count > 1 || (mType & SaveInfo.SAVE_DATA_TYPE_GENERIC_CARD) != 0) { 253 types.add(context.getString(R.string.autofill_save_type_generic_card)); 254 } else if ((mType & SaveInfo.SAVE_DATA_TYPE_PAYMENT_CARD) != 0) { 255 types.add(context.getString(R.string.autofill_save_type_payment_card)); 256 } else if ((mType & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) { 257 types.add(context.getString(R.string.autofill_save_type_credit_card)); 258 } else if ((mType & SaveInfo.SAVE_DATA_TYPE_DEBIT_CARD) != 0) { 259 types.add(context.getString(R.string.autofill_save_type_debit_card)); 260 } 261 if ((mType & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) { 262 types.add(context.getString(R.string.autofill_save_type_username)); 263 } 264 if ((mType & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) { 265 types.add(context.getString(R.string.autofill_save_type_email_address)); 266 } 267 268 switch (types.size()) { 269 case 1: 270 mTitle = Html.fromHtml(context.getString( 271 isUpdate ? R.string.autofill_update_title_with_type 272 : R.string.autofill_save_title_with_type, 273 types.valueAt(0), serviceLabel), 0); 274 break; 275 case 2: 276 mTitle = Html.fromHtml(context.getString( 277 isUpdate ? R.string.autofill_update_title_with_2types 278 : R.string.autofill_save_title_with_2types, 279 types.valueAt(0), types.valueAt(1), serviceLabel), 0); 280 break; 281 case 3: 282 mTitle = Html.fromHtml(context.getString( 283 isUpdate ? R.string.autofill_update_title_with_3types 284 : R.string.autofill_save_title_with_3types, 285 types.valueAt(0), types.valueAt(1), types.valueAt(2), serviceLabel), 0); 286 break; 287 default: 288 // Use generic if more than 3 or invalid type (size 0). 289 mTitle = Html.fromHtml( 290 context.getString(isUpdate ? R.string.autofill_update_title 291 : R.string.autofill_save_title, serviceLabel), 292 0); 293 } 294 titleView.setText(mTitle); 295 296 if (showServiceIcon) { 297 setServiceIcon(context, view, serviceIcon); 298 } 299 300 final boolean hasCustomDescription = 301 applyCustomDescription(context, view, valueFinder, info); 302 if (hasCustomDescription) { 303 mSubTitle = null; 304 if (sDebug) Slog.d(TAG, "on constructor: applied custom description"); 305 } else { 306 mSubTitle = info.getDescription(); 307 if (mSubTitle != null) { 308 writeLog(MetricsEvent.AUTOFILL_SAVE_CUSTOM_SUBTITLE); 309 final ViewGroup subtitleContainer = 310 view.findViewById(R.id.autofill_save_custom_subtitle); 311 final TextView subtitleView = new TextView(context); 312 subtitleView.setText(mSubTitle); 313 applyMovementMethodIfNeed(subtitleView); 314 subtitleContainer.addView(subtitleView, 315 new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 316 ViewGroup.LayoutParams.WRAP_CONTENT)); 317 subtitleContainer.setVisibility(View.VISIBLE); 318 subtitleContainer.setScrollBarDefaultDelayBeforeFade( 319 SCROLL_BAR_DEFAULT_DELAY_BEFORE_FADE_MS); 320 } 321 if (sDebug) Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); 322 } 323 324 final TextView noButton = view.findViewById(R.id.autofill_save_no); 325 final int negativeActionStyle = info.getNegativeActionStyle(); 326 switch (negativeActionStyle) { 327 case SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT: 328 noButton.setText(R.string.autofill_save_notnow); 329 break; 330 case SaveInfo.NEGATIVE_BUTTON_STYLE_NEVER: 331 noButton.setText(R.string.autofill_save_never); 332 break; 333 case SaveInfo.NEGATIVE_BUTTON_STYLE_CANCEL: 334 default: 335 noButton.setText(R.string.autofill_save_no); 336 } 337 noButton.setOnClickListener((v) -> mListener.onCancel(info.getNegativeActionListener())); 338 339 final TextView yesButton = view.findViewById(R.id.autofill_save_yes); 340 if (info.getPositiveActionStyle() == SaveInfo.POSITIVE_BUTTON_STYLE_CONTINUE) { 341 yesButton.setText(R.string.autofill_continue_yes); 342 } else if (isUpdate) { 343 yesButton.setText(R.string.autofill_update_yes); 344 } 345 yesButton.setOnClickListener((v) -> mListener.onSave()); 346 347 mDialog = new Dialog(context, mThemeId); 348 mDialog.setContentView(view); 349 350 // Dialog can be dismissed when touched outside, but the negative listener should not be 351 // notified (hence the null argument). 352 mDialog.setOnDismissListener((d) -> mListener.onCancel(null)); 353 354 final Window window = mDialog.getWindow(); 355 window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 356 window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 357 | WindowManager.LayoutParams.FLAG_DIM_BEHIND); 358 window.setDimAmount(0.6f); 359 window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); 360 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); 361 window.setGravity(Gravity.BOTTOM | Gravity.CENTER); 362 window.setCloseOnTouchOutside(true); 363 final WindowManager.LayoutParams params = window.getAttributes(); 364 365 params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); 366 params.windowAnimations = R.style.AutofillSaveAnimation; 367 params.setTrustedOverlay(); 368 369 show(); 370 } 371 applyCustomDescription(@onNull Context context, @NonNull View saveUiView, @NonNull ValueFinder valueFinder, @NonNull SaveInfo info)372 private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView, 373 @NonNull ValueFinder valueFinder, @NonNull SaveInfo info) { 374 final CustomDescription customDescription = info.getCustomDescription(); 375 if (customDescription == null) { 376 return false; 377 } 378 writeLog(MetricsEvent.AUTOFILL_SAVE_CUSTOM_DESCRIPTION); 379 final RemoteViews template = Helper.sanitizeRemoteView(customDescription.getPresentation()); 380 if (template == null) { 381 Slog.w(TAG, "No remote view on custom description"); 382 return false; 383 } 384 385 // First apply the unconditional transformations (if any) to the templates. 386 final ArrayList<Pair<Integer, InternalTransformation>> transformations = 387 customDescription.getTransformations(); 388 if (sVerbose) { 389 Slog.v(TAG, "applyCustomDescription(): transformations = " + transformations); 390 } 391 if (transformations != null) { 392 if (!InternalTransformation.batchApply(valueFinder, template, transformations)) { 393 Slog.w(TAG, "could not apply main transformations on custom description"); 394 return false; 395 } 396 } 397 398 final RemoteViews.InteractionHandler handler = 399 (view, pendingIntent, response) -> { 400 Intent intent = response.getLaunchOptions(view).first; 401 final boolean isValid = isValidLink(pendingIntent, intent); 402 if (!isValid) { 403 final LogMaker log = 404 newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, mType); 405 log.setType(MetricsEvent.TYPE_UNKNOWN); 406 mMetricsLogger.write(log); 407 return false; 408 } 409 410 startIntentSenderWithRestore(pendingIntent, intent); 411 return true; 412 }; 413 414 try { 415 // Create the remote view peer. 416 final View customSubtitleView = template.applyWithTheme( 417 context, null, handler, mThemeId); 418 419 // Apply batch updates (if any). 420 final ArrayList<Pair<InternalValidator, BatchUpdates>> updates = 421 customDescription.getUpdates(); 422 if (sVerbose) { 423 Slog.v(TAG, "applyCustomDescription(): view = " + customSubtitleView 424 + " updates=" + updates); 425 } 426 if (updates != null) { 427 final int size = updates.size(); 428 if (sDebug) Slog.d(TAG, "custom description has " + size + " batch updates"); 429 for (int i = 0; i < size; i++) { 430 final Pair<InternalValidator, BatchUpdates> pair = updates.get(i); 431 final InternalValidator condition = pair.first; 432 if (condition == null || !condition.isValid(valueFinder)) { 433 if (sDebug) Slog.d(TAG, "Skipping batch update #" + i ); 434 continue; 435 } 436 final BatchUpdates batchUpdates = pair.second; 437 // First apply the updates... 438 final RemoteViews templateUpdates = 439 Helper.sanitizeRemoteView(batchUpdates.getUpdates()); 440 if (templateUpdates != null) { 441 if (sDebug) Slog.d(TAG, "Applying template updates for batch update #" + i); 442 templateUpdates.reapply(context, customSubtitleView); 443 } 444 // Then the transformations... 445 final ArrayList<Pair<Integer, InternalTransformation>> batchTransformations = 446 batchUpdates.getTransformations(); 447 if (batchTransformations != null) { 448 if (sDebug) { 449 Slog.d(TAG, "Applying child transformation for batch update #" + i 450 + ": " + batchTransformations); 451 } 452 if (!InternalTransformation.batchApply(valueFinder, template, 453 batchTransformations)) { 454 Slog.w(TAG, "Could not apply child transformation for batch update " 455 + "#" + i + ": " + batchTransformations); 456 return false; 457 } 458 template.reapply(context, customSubtitleView); 459 } 460 } 461 } 462 463 // Apply click actions (if any). 464 final SparseArray<InternalOnClickAction> actions = customDescription.getActions(); 465 if (actions != null) { 466 final int size = actions.size(); 467 if (sDebug) Slog.d(TAG, "custom description has " + size + " actions"); 468 if (!(customSubtitleView instanceof ViewGroup)) { 469 Slog.w(TAG, "cannot apply actions because custom description root is not a " 470 + "ViewGroup: " + customSubtitleView); 471 } else { 472 final ViewGroup rootView = (ViewGroup) customSubtitleView; 473 for (int i = 0; i < size; i++) { 474 final int id = actions.keyAt(i); 475 final InternalOnClickAction action = actions.valueAt(i); 476 final View child = rootView.findViewById(id); 477 if (child == null) { 478 Slog.w(TAG, "Ignoring action " + action + " for view " + id 479 + " because it's not on " + rootView); 480 continue; 481 } 482 child.setOnClickListener((v) -> { 483 if (sVerbose) { 484 Slog.v(TAG, "Applying " + action + " after " + v + " was clicked"); 485 } 486 action.onClick(rootView); 487 }); 488 } 489 } 490 } 491 492 applyTextViewStyle(customSubtitleView); 493 494 // Finally, add the custom description to the save UI. 495 final ViewGroup subtitleContainer = 496 saveUiView.findViewById(R.id.autofill_save_custom_subtitle); 497 subtitleContainer.addView(customSubtitleView); 498 subtitleContainer.setVisibility(View.VISIBLE); 499 subtitleContainer.setScrollBarDefaultDelayBeforeFade( 500 SCROLL_BAR_DEFAULT_DELAY_BEFORE_FADE_MS); 501 502 return true; 503 } catch (Exception e) { 504 Slog.e(TAG, "Error applying custom description. ", e); 505 } 506 return false; 507 } 508 startIntentSenderWithRestore(@onNull PendingIntent pendingIntent, @NonNull Intent intent)509 private void startIntentSenderWithRestore(@NonNull PendingIntent pendingIntent, 510 @NonNull Intent intent) { 511 if (sVerbose) Slog.v(TAG, "Intercepting custom description intent"); 512 513 // We need to hide the Save UI before launching the pending intent, and 514 // restore back it once the activity is finished, and that's achieved by 515 // adding a custom extra in the activity intent. 516 final IBinder token = mPendingUi.getToken(); 517 intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); 518 519 mListener.startIntentSender(pendingIntent.getIntentSender(), intent); 520 mPendingUi.setState(PendingUi.STATE_PENDING); 521 522 if (sDebug) Slog.d(TAG, "hiding UI until restored with token " + token); 523 hide(); 524 525 final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, mType); 526 log.setType(MetricsEvent.TYPE_OPEN); 527 mMetricsLogger.write(log); 528 } 529 applyTextViewStyle(@onNull View rootView)530 private void applyTextViewStyle(@NonNull View rootView) { 531 final List<TextView> textViews = new ArrayList<>(); 532 final Predicate<View> predicate = (view) -> { 533 if (view instanceof TextView) { 534 // Collects TextViews 535 textViews.add((TextView) view); 536 } 537 return false; 538 }; 539 540 // Traverses all TextViews, enables movement method if the TextView contains URLSpan 541 rootView.findViewByPredicate(predicate); 542 final int size = textViews.size(); 543 for (int i = 0; i < size; i++) { 544 applyMovementMethodIfNeed(textViews.get(i)); 545 } 546 } 547 applyMovementMethodIfNeed(@onNull TextView textView)548 private void applyMovementMethodIfNeed(@NonNull TextView textView) { 549 final CharSequence message = textView.getText(); 550 if (TextUtils.isEmpty(message)) { 551 return; 552 } 553 554 final SpannableStringBuilder ssb = new SpannableStringBuilder(message); 555 final ClickableSpan[] spans = ssb.getSpans(0, ssb.length(), ClickableSpan.class); 556 if (ArrayUtils.isEmpty(spans)) { 557 return; 558 } 559 560 textView.setMovementMethod(LinkMovementMethod.getInstance()); 561 } 562 setServiceIcon(Context context, View view, Drawable serviceIcon)563 private void setServiceIcon(Context context, View view, Drawable serviceIcon) { 564 final ImageView iconView = view.findViewById(R.id.autofill_save_icon); 565 final Resources res = context.getResources(); 566 iconView.setImageDrawable(serviceIcon); 567 } 568 isValidLink(PendingIntent pendingIntent, Intent intent)569 private static boolean isValidLink(PendingIntent pendingIntent, Intent intent) { 570 if (pendingIntent == null) { 571 Slog.w(TAG, "isValidLink(): custom description without pending intent"); 572 return false; 573 } 574 if (!pendingIntent.isActivity()) { 575 Slog.w(TAG, "isValidLink(): pending intent not for activity"); 576 return false; 577 } 578 if (intent == null) { 579 Slog.w(TAG, "isValidLink(): no intent"); 580 return false; 581 } 582 return true; 583 } 584 newLogMaker(int category, int saveType)585 private LogMaker newLogMaker(int category, int saveType) { 586 return newLogMaker(category).addTaggedData(MetricsEvent.FIELD_AUTOFILL_SAVE_TYPE, saveType); 587 } 588 newLogMaker(int category)589 private LogMaker newLogMaker(int category) { 590 return Helper.newLogMaker(category, mComponentName, mServicePackageName, 591 mPendingUi.sessionId, mCompatMode); 592 } 593 writeLog(int category)594 private void writeLog(int category) { 595 mMetricsLogger.write(newLogMaker(category, mType)); 596 } 597 598 /** 599 * Update the pending UI, if any. 600 * 601 * @param operation how to update it. 602 * @param token token associated with the pending UI - if it doesn't match the pending token, 603 * the operation will be ignored. 604 */ onPendingUi(int operation, @NonNull IBinder token)605 void onPendingUi(int operation, @NonNull IBinder token) { 606 if (!mPendingUi.matches(token)) { 607 Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of " 608 + mPendingUi.getToken()); 609 return; 610 } 611 final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_PENDING_SAVE_UI_OPERATION); 612 try { 613 switch (operation) { 614 case AutofillManager.PENDING_UI_OPERATION_RESTORE: 615 if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token); 616 log.setType(MetricsEvent.TYPE_OPEN); 617 show(); 618 break; 619 case AutofillManager.PENDING_UI_OPERATION_CANCEL: 620 log.setType(MetricsEvent.TYPE_DISMISS); 621 if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token); 622 hide(); 623 break; 624 default: 625 log.setType(MetricsEvent.TYPE_FAILURE); 626 Slog.w(TAG, "restore(): invalid operation " + operation); 627 } 628 } finally { 629 mMetricsLogger.write(log); 630 } 631 mPendingUi.setState(PendingUi.STATE_FINISHED); 632 } 633 show()634 private void show() { 635 Slog.i(TAG, "Showing save dialog: " + mTitle); 636 mDialog.show(); 637 mOverlayControl.hideOverlays(); 638 } 639 hide()640 PendingUi hide() { 641 if (sVerbose) Slog.v(TAG, "Hiding save dialog."); 642 try { 643 mDialog.hide(); 644 } finally { 645 mOverlayControl.showOverlays(); 646 } 647 return mPendingUi; 648 } 649 isShowing()650 boolean isShowing() { 651 return mDialog.isShowing(); 652 } 653 destroy()654 void destroy() { 655 try { 656 if (sDebug) Slog.d(TAG, "destroy()"); 657 throwIfDestroyed(); 658 mListener.onDestroy(); 659 mHandler.removeCallbacksAndMessages(mListener); 660 mDialog.dismiss(); 661 mDestroyed = true; 662 } finally { 663 mOverlayControl.showOverlays(); 664 } 665 } 666 throwIfDestroyed()667 private void throwIfDestroyed() { 668 if (mDestroyed) { 669 throw new IllegalStateException("cannot interact with a destroyed instance"); 670 } 671 } 672 673 @Override toString()674 public String toString() { 675 return mTitle == null ? "NO TITLE" : mTitle.toString(); 676 } 677 dump(PrintWriter pw, String prefix)678 void dump(PrintWriter pw, String prefix) { 679 pw.print(prefix); pw.print("title: "); pw.println(mTitle); 680 pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); 681 pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi); 682 pw.print(prefix); pw.print("service: "); pw.println(mServicePackageName); 683 pw.print(prefix); pw.print("app: "); pw.println(mComponentName.toShortString()); 684 pw.print(prefix); pw.print("compat mode: "); pw.println(mCompatMode); 685 pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId); 686 switch (mThemeId) { 687 case THEME_ID_DARK: 688 pw.println(" (dark)"); 689 break; 690 case THEME_ID_LIGHT: 691 pw.println(" (light)"); 692 break; 693 default: 694 pw.println("(UNKNOWN_MODE)"); 695 break; 696 } 697 final View view = mDialog.getWindow().getDecorView(); 698 final int[] loc = view.getLocationOnScreen(); 699 pw.print(prefix); pw.print("coordinates: "); 700 pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')'); 701 pw.print('('); 702 pw.print(loc[0] + view.getWidth()); pw.print(','); 703 pw.print(loc[1] + view.getHeight());pw.println(')'); 704 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 705 } 706 } 707