• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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