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