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