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