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