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