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