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 = batchUpdates.getUpdates(); 439 if (templateUpdates != null) { 440 if (sDebug) Slog.d(TAG, "Applying template updates for batch update #" + i); 441 templateUpdates.reapply(context, customSubtitleView); 442 } 443 // Then the transformations... 444 final ArrayList<Pair<Integer, InternalTransformation>> batchTransformations = 445 batchUpdates.getTransformations(); 446 if (batchTransformations != null) { 447 if (sDebug) { 448 Slog.d(TAG, "Applying child transformation for batch update #" + i 449 + ": " + batchTransformations); 450 } 451 if (!InternalTransformation.batchApply(valueFinder, template, 452 batchTransformations)) { 453 Slog.w(TAG, "Could not apply child transformation for batch update " 454 + "#" + i + ": " + batchTransformations); 455 return false; 456 } 457 template.reapply(context, customSubtitleView); 458 } 459 } 460 } 461 462 // Apply click actions (if any). 463 final SparseArray<InternalOnClickAction> actions = customDescription.getActions(); 464 if (actions != null) { 465 final int size = actions.size(); 466 if (sDebug) Slog.d(TAG, "custom description has " + size + " actions"); 467 if (!(customSubtitleView instanceof ViewGroup)) { 468 Slog.w(TAG, "cannot apply actions because custom description root is not a " 469 + "ViewGroup: " + customSubtitleView); 470 } else { 471 final ViewGroup rootView = (ViewGroup) customSubtitleView; 472 for (int i = 0; i < size; i++) { 473 final int id = actions.keyAt(i); 474 final InternalOnClickAction action = actions.valueAt(i); 475 final View child = rootView.findViewById(id); 476 if (child == null) { 477 Slog.w(TAG, "Ignoring action " + action + " for view " + id 478 + " because it's not on " + rootView); 479 continue; 480 } 481 child.setOnClickListener((v) -> { 482 if (sVerbose) { 483 Slog.v(TAG, "Applying " + action + " after " + v + " was clicked"); 484 } 485 action.onClick(rootView); 486 }); 487 } 488 } 489 } 490 491 applyTextViewStyle(customSubtitleView); 492 493 // Finally, add the custom description to the save UI. 494 final ViewGroup subtitleContainer = 495 saveUiView.findViewById(R.id.autofill_save_custom_subtitle); 496 subtitleContainer.addView(customSubtitleView); 497 subtitleContainer.setVisibility(View.VISIBLE); 498 subtitleContainer.setScrollBarDefaultDelayBeforeFade( 499 SCROLL_BAR_DEFAULT_DELAY_BEFORE_FADE_MS); 500 501 return true; 502 } catch (Exception e) { 503 Slog.e(TAG, "Error applying custom description. ", e); 504 } 505 return false; 506 } 507 startIntentSenderWithRestore(@onNull PendingIntent pendingIntent, @NonNull Intent intent)508 private void startIntentSenderWithRestore(@NonNull PendingIntent pendingIntent, 509 @NonNull Intent intent) { 510 if (sVerbose) Slog.v(TAG, "Intercepting custom description intent"); 511 512 // We need to hide the Save UI before launching the pending intent, and 513 // restore back it once the activity is finished, and that's achieved by 514 // adding a custom extra in the activity intent. 515 final IBinder token = mPendingUi.getToken(); 516 intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token); 517 518 mListener.startIntentSender(pendingIntent.getIntentSender(), intent); 519 mPendingUi.setState(PendingUi.STATE_PENDING); 520 521 if (sDebug) Slog.d(TAG, "hiding UI until restored with token " + token); 522 hide(); 523 524 final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, mType); 525 log.setType(MetricsEvent.TYPE_OPEN); 526 mMetricsLogger.write(log); 527 } 528 applyTextViewStyle(@onNull View rootView)529 private void applyTextViewStyle(@NonNull View rootView) { 530 final List<TextView> textViews = new ArrayList<>(); 531 final Predicate<View> predicate = (view) -> { 532 if (view instanceof TextView) { 533 // Collects TextViews 534 textViews.add((TextView) view); 535 } 536 return false; 537 }; 538 539 // Traverses all TextViews, enables movement method if the TextView contains URLSpan 540 rootView.findViewByPredicate(predicate); 541 final int size = textViews.size(); 542 for (int i = 0; i < size; i++) { 543 applyMovementMethodIfNeed(textViews.get(i)); 544 } 545 } 546 applyMovementMethodIfNeed(@onNull TextView textView)547 private void applyMovementMethodIfNeed(@NonNull TextView textView) { 548 final CharSequence message = textView.getText(); 549 if (TextUtils.isEmpty(message)) { 550 return; 551 } 552 553 final SpannableStringBuilder ssb = new SpannableStringBuilder(message); 554 final ClickableSpan[] spans = ssb.getSpans(0, ssb.length(), ClickableSpan.class); 555 if (ArrayUtils.isEmpty(spans)) { 556 return; 557 } 558 559 textView.setMovementMethod(LinkMovementMethod.getInstance()); 560 } 561 setServiceIcon(Context context, View view, Drawable serviceIcon)562 private void setServiceIcon(Context context, View view, Drawable serviceIcon) { 563 final ImageView iconView = view.findViewById(R.id.autofill_save_icon); 564 final Resources res = context.getResources(); 565 iconView.setImageDrawable(serviceIcon); 566 } 567 isValidLink(PendingIntent pendingIntent, Intent intent)568 private static boolean isValidLink(PendingIntent pendingIntent, Intent intent) { 569 if (pendingIntent == null) { 570 Slog.w(TAG, "isValidLink(): custom description without pending intent"); 571 return false; 572 } 573 if (!pendingIntent.isActivity()) { 574 Slog.w(TAG, "isValidLink(): pending intent not for activity"); 575 return false; 576 } 577 if (intent == null) { 578 Slog.w(TAG, "isValidLink(): no intent"); 579 return false; 580 } 581 return true; 582 } 583 newLogMaker(int category, int saveType)584 private LogMaker newLogMaker(int category, int saveType) { 585 return newLogMaker(category).addTaggedData(MetricsEvent.FIELD_AUTOFILL_SAVE_TYPE, saveType); 586 } 587 newLogMaker(int category)588 private LogMaker newLogMaker(int category) { 589 return Helper.newLogMaker(category, mComponentName, mServicePackageName, 590 mPendingUi.sessionId, mCompatMode); 591 } 592 writeLog(int category)593 private void writeLog(int category) { 594 mMetricsLogger.write(newLogMaker(category, mType)); 595 } 596 597 /** 598 * Update the pending UI, if any. 599 * 600 * @param operation how to update it. 601 * @param token token associated with the pending UI - if it doesn't match the pending token, 602 * the operation will be ignored. 603 */ onPendingUi(int operation, @NonNull IBinder token)604 void onPendingUi(int operation, @NonNull IBinder token) { 605 if (!mPendingUi.matches(token)) { 606 Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of " 607 + mPendingUi.getToken()); 608 return; 609 } 610 final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_PENDING_SAVE_UI_OPERATION); 611 try { 612 switch (operation) { 613 case AutofillManager.PENDING_UI_OPERATION_RESTORE: 614 if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token); 615 log.setType(MetricsEvent.TYPE_OPEN); 616 show(); 617 break; 618 case AutofillManager.PENDING_UI_OPERATION_CANCEL: 619 log.setType(MetricsEvent.TYPE_DISMISS); 620 if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token); 621 hide(); 622 break; 623 default: 624 log.setType(MetricsEvent.TYPE_FAILURE); 625 Slog.w(TAG, "restore(): invalid operation " + operation); 626 } 627 } finally { 628 mMetricsLogger.write(log); 629 } 630 mPendingUi.setState(PendingUi.STATE_FINISHED); 631 } 632 show()633 private void show() { 634 Slog.i(TAG, "Showing save dialog: " + mTitle); 635 mDialog.show(); 636 mOverlayControl.hideOverlays(); 637 } 638 hide()639 PendingUi hide() { 640 if (sVerbose) Slog.v(TAG, "Hiding save dialog."); 641 try { 642 mDialog.hide(); 643 } finally { 644 mOverlayControl.showOverlays(); 645 } 646 return mPendingUi; 647 } 648 isShowing()649 boolean isShowing() { 650 return mDialog.isShowing(); 651 } 652 destroy()653 void destroy() { 654 try { 655 if (sDebug) Slog.d(TAG, "destroy()"); 656 throwIfDestroyed(); 657 mListener.onDestroy(); 658 mHandler.removeCallbacksAndMessages(mListener); 659 mDialog.dismiss(); 660 mDestroyed = true; 661 } finally { 662 mOverlayControl.showOverlays(); 663 } 664 } 665 throwIfDestroyed()666 private void throwIfDestroyed() { 667 if (mDestroyed) { 668 throw new IllegalStateException("cannot interact with a destroyed instance"); 669 } 670 } 671 672 @Override toString()673 public String toString() { 674 return mTitle == null ? "NO TITLE" : mTitle.toString(); 675 } 676 dump(PrintWriter pw, String prefix)677 void dump(PrintWriter pw, String prefix) { 678 pw.print(prefix); pw.print("title: "); pw.println(mTitle); 679 pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); 680 pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi); 681 pw.print(prefix); pw.print("service: "); pw.println(mServicePackageName); 682 pw.print(prefix); pw.print("app: "); pw.println(mComponentName.toShortString()); 683 pw.print(prefix); pw.print("compat mode: "); pw.println(mCompatMode); 684 pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId); 685 switch (mThemeId) { 686 case THEME_ID_DARK: 687 pw.println(" (dark)"); 688 break; 689 case THEME_ID_LIGHT: 690 pw.println(" (light)"); 691 break; 692 default: 693 pw.println("(UNKNOWN_MODE)"); 694 break; 695 } 696 final View view = mDialog.getWindow().getDecorView(); 697 final int[] loc = view.getLocationOnScreen(); 698 pw.print(prefix); pw.print("coordinates: "); 699 pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')'); 700 pw.print('('); 701 pw.print(loc[0] + view.getWidth()); pw.print(','); 702 pw.print(loc[1] + view.getHeight());pw.println(')'); 703 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 704 } 705 } 706