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