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 =
439                             Helper.sanitizeRemoteView(batchUpdates.getUpdates());
440                     if (templateUpdates != null) {
441                         if (sDebug) Slog.d(TAG, "Applying template updates for batch update #" + i);
442                         templateUpdates.reapply(context, customSubtitleView);
443                     }
444                     // Then the transformations...
445                     final ArrayList<Pair<Integer, InternalTransformation>> batchTransformations =
446                             batchUpdates.getTransformations();
447                     if (batchTransformations != null) {
448                         if (sDebug) {
449                             Slog.d(TAG, "Applying child transformation for batch update #" + i
450                                     + ": " + batchTransformations);
451                         }
452                         if (!InternalTransformation.batchApply(valueFinder, template,
453                                 batchTransformations)) {
454                             Slog.w(TAG, "Could not apply child transformation for batch update "
455                                     + "#" + i + ": " + batchTransformations);
456                             return false;
457                         }
458                         template.reapply(context, customSubtitleView);
459                     }
460                 }
461             }
462 
463             // Apply click actions (if any).
464             final SparseArray<InternalOnClickAction> actions = customDescription.getActions();
465             if (actions != null) {
466                 final int size = actions.size();
467                 if (sDebug) Slog.d(TAG, "custom description has " + size + " actions");
468                 if (!(customSubtitleView instanceof ViewGroup)) {
469                     Slog.w(TAG, "cannot apply actions because custom description root is not a "
470                             + "ViewGroup: " + customSubtitleView);
471                 } else {
472                     final ViewGroup rootView = (ViewGroup) customSubtitleView;
473                     for (int i = 0; i < size; i++) {
474                         final int id = actions.keyAt(i);
475                         final InternalOnClickAction action = actions.valueAt(i);
476                         final View child = rootView.findViewById(id);
477                         if (child == null) {
478                             Slog.w(TAG, "Ignoring action " + action + " for view " + id
479                                     + " because it's not on " + rootView);
480                             continue;
481                         }
482                         child.setOnClickListener((v) -> {
483                             if (sVerbose) {
484                                 Slog.v(TAG, "Applying " + action + " after " + v + " was clicked");
485                             }
486                             action.onClick(rootView);
487                         });
488                     }
489                 }
490             }
491 
492             applyTextViewStyle(customSubtitleView);
493 
494             // Finally, add the custom description to the save UI.
495             final ViewGroup subtitleContainer =
496                     saveUiView.findViewById(R.id.autofill_save_custom_subtitle);
497             subtitleContainer.addView(customSubtitleView);
498             subtitleContainer.setVisibility(View.VISIBLE);
499             subtitleContainer.setScrollBarDefaultDelayBeforeFade(
500                     SCROLL_BAR_DEFAULT_DELAY_BEFORE_FADE_MS);
501 
502             return true;
503         } catch (Exception e) {
504             Slog.e(TAG, "Error applying custom description. ", e);
505         }
506         return false;
507     }
508 
startIntentSenderWithRestore(@onNull PendingIntent pendingIntent, @NonNull Intent intent)509     private void startIntentSenderWithRestore(@NonNull PendingIntent pendingIntent,
510             @NonNull Intent intent) {
511         if (sVerbose) Slog.v(TAG, "Intercepting custom description intent");
512 
513         // We need to hide the Save UI before launching the pending intent, and
514         // restore back it once the activity is finished, and that's achieved by
515         // adding a custom extra in the activity intent.
516         final IBinder token = mPendingUi.getToken();
517         intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token);
518 
519         mListener.startIntentSender(pendingIntent.getIntentSender(), intent);
520         mPendingUi.setState(PendingUi.STATE_PENDING);
521 
522         if (sDebug) Slog.d(TAG, "hiding UI until restored with token " + token);
523         hide();
524 
525         final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_LINK_TAPPED, mType);
526         log.setType(MetricsEvent.TYPE_OPEN);
527         mMetricsLogger.write(log);
528     }
529 
applyTextViewStyle(@onNull View rootView)530     private void applyTextViewStyle(@NonNull View rootView) {
531         final List<TextView> textViews = new ArrayList<>();
532         final Predicate<View> predicate = (view) -> {
533             if (view instanceof TextView) {
534                 // Collects TextViews
535                 textViews.add((TextView) view);
536             }
537             return false;
538         };
539 
540         // Traverses all TextViews, enables movement method if the TextView contains URLSpan
541         rootView.findViewByPredicate(predicate);
542         final int size = textViews.size();
543         for (int i = 0; i < size; i++) {
544             applyMovementMethodIfNeed(textViews.get(i));
545         }
546     }
547 
applyMovementMethodIfNeed(@onNull TextView textView)548     private void applyMovementMethodIfNeed(@NonNull TextView textView) {
549         final CharSequence message = textView.getText();
550         if (TextUtils.isEmpty(message)) {
551             return;
552         }
553 
554         final SpannableStringBuilder ssb = new SpannableStringBuilder(message);
555         final ClickableSpan[] spans = ssb.getSpans(0, ssb.length(), ClickableSpan.class);
556         if (ArrayUtils.isEmpty(spans)) {
557             return;
558         }
559 
560         textView.setMovementMethod(LinkMovementMethod.getInstance());
561     }
562 
setServiceIcon(Context context, View view, Drawable serviceIcon)563     private void setServiceIcon(Context context, View view, Drawable serviceIcon) {
564         final ImageView iconView = view.findViewById(R.id.autofill_save_icon);
565         final Resources res = context.getResources();
566         iconView.setImageDrawable(serviceIcon);
567     }
568 
isValidLink(PendingIntent pendingIntent, Intent intent)569     private static boolean isValidLink(PendingIntent pendingIntent, Intent intent) {
570         if (pendingIntent == null) {
571             Slog.w(TAG, "isValidLink(): custom description without pending intent");
572             return false;
573         }
574         if (!pendingIntent.isActivity()) {
575             Slog.w(TAG, "isValidLink(): pending intent not for activity");
576             return false;
577         }
578         if (intent == null) {
579             Slog.w(TAG, "isValidLink(): no intent");
580             return false;
581         }
582         return true;
583     }
584 
newLogMaker(int category, int saveType)585     private LogMaker newLogMaker(int category, int saveType) {
586         return newLogMaker(category).addTaggedData(MetricsEvent.FIELD_AUTOFILL_SAVE_TYPE, saveType);
587     }
588 
newLogMaker(int category)589     private LogMaker newLogMaker(int category) {
590         return Helper.newLogMaker(category, mComponentName, mServicePackageName,
591                 mPendingUi.sessionId, mCompatMode);
592     }
593 
writeLog(int category)594     private void writeLog(int category) {
595         mMetricsLogger.write(newLogMaker(category, mType));
596     }
597 
598     /**
599      * Update the pending UI, if any.
600      *
601      * @param operation how to update it.
602      * @param token token associated with the pending UI - if it doesn't match the pending token,
603      * the operation will be ignored.
604      */
onPendingUi(int operation, @NonNull IBinder token)605     void onPendingUi(int operation, @NonNull IBinder token) {
606         if (!mPendingUi.matches(token)) {
607             Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of "
608                     + mPendingUi.getToken());
609             return;
610         }
611         final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_PENDING_SAVE_UI_OPERATION);
612         try {
613             switch (operation) {
614                 case AutofillManager.PENDING_UI_OPERATION_RESTORE:
615                     if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token);
616                     log.setType(MetricsEvent.TYPE_OPEN);
617                     show();
618                     break;
619                 case AutofillManager.PENDING_UI_OPERATION_CANCEL:
620                     log.setType(MetricsEvent.TYPE_DISMISS);
621                     if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token);
622                     hide();
623                     break;
624                 default:
625                     log.setType(MetricsEvent.TYPE_FAILURE);
626                     Slog.w(TAG, "restore(): invalid operation " + operation);
627             }
628         } finally {
629             mMetricsLogger.write(log);
630         }
631         mPendingUi.setState(PendingUi.STATE_FINISHED);
632     }
633 
show()634     private void show() {
635         Slog.i(TAG, "Showing save dialog: " + mTitle);
636         mDialog.show();
637         mOverlayControl.hideOverlays();
638    }
639 
hide()640     PendingUi hide() {
641         if (sVerbose) Slog.v(TAG, "Hiding save dialog.");
642         try {
643             mDialog.hide();
644         } finally {
645             mOverlayControl.showOverlays();
646         }
647         return mPendingUi;
648     }
649 
isShowing()650     boolean isShowing() {
651         return mDialog.isShowing();
652     }
653 
destroy()654     void destroy() {
655         try {
656             if (sDebug) Slog.d(TAG, "destroy()");
657             throwIfDestroyed();
658             mListener.onDestroy();
659             mHandler.removeCallbacksAndMessages(mListener);
660             mDialog.dismiss();
661             mDestroyed = true;
662         } finally {
663             mOverlayControl.showOverlays();
664         }
665     }
666 
throwIfDestroyed()667     private void throwIfDestroyed() {
668         if (mDestroyed) {
669             throw new IllegalStateException("cannot interact with a destroyed instance");
670         }
671     }
672 
673     @Override
toString()674     public String toString() {
675         return mTitle == null ? "NO TITLE" : mTitle.toString();
676     }
677 
dump(PrintWriter pw, String prefix)678     void dump(PrintWriter pw, String prefix) {
679         pw.print(prefix); pw.print("title: "); pw.println(mTitle);
680         pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle);
681         pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi);
682         pw.print(prefix); pw.print("service: "); pw.println(mServicePackageName);
683         pw.print(prefix); pw.print("app: "); pw.println(mComponentName.toShortString());
684         pw.print(prefix); pw.print("compat mode: "); pw.println(mCompatMode);
685         pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId);
686         switch (mThemeId) {
687             case THEME_ID_DARK:
688                 pw.println(" (dark)");
689                 break;
690             case THEME_ID_LIGHT:
691                 pw.println(" (light)");
692                 break;
693             default:
694                 pw.println("(UNKNOWN_MODE)");
695                 break;
696         }
697         final View view = mDialog.getWindow().getDecorView();
698         final int[] loc = view.getLocationOnScreen();
699         pw.print(prefix); pw.print("coordinates: ");
700             pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')');
701             pw.print('(');
702                 pw.print(loc[0] + view.getWidth()); pw.print(',');
703                 pw.print(loc[1] + view.getHeight());pw.println(')');
704         pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
705     }
706 }
707