SaveUi.java revision 838ba3dbbd0753b3bbf7cff6232e57c488cfdf2d
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
17package com.android.server.autofill.ui;
18
19import static com.android.server.autofill.Helper.sDebug;
20import static com.android.server.autofill.Helper.sVerbose;
21
22import android.annotation.NonNull;
23import android.app.Dialog;
24import android.app.PendingIntent;
25import android.app.PendingIntent.CanceledException;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentSender;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.service.autofill.CustomDescription;
33import android.service.autofill.SaveInfo;
34import android.service.autofill.ValueFinder;
35import android.text.Html;
36import android.util.ArraySet;
37import android.util.Slog;
38import android.view.Gravity;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewGroup.LayoutParams;
43import android.view.Window;
44import android.view.WindowManager;
45import android.view.autofill.AutofillManager;
46import android.view.autofill.IAutoFillManagerClient;
47import android.widget.RemoteViews;
48import android.widget.ScrollView;
49import android.widget.TextView;
50
51import com.android.internal.R;
52import com.android.server.UiThread;
53
54import java.io.PrintWriter;
55
56/**
57 * Autofill Save Prompt
58 */
59final class SaveUi {
60
61    private static final String TAG = "AutofillSaveUi";
62
63    public interface OnSaveListener {
64        void onSave();
65        void onCancel(IntentSender listener);
66        void onDestroy();
67    }
68
69    private class OneTimeListener implements OnSaveListener {
70
71        private final OnSaveListener mRealListener;
72        private boolean mDone;
73
74        OneTimeListener(OnSaveListener realListener) {
75            mRealListener = realListener;
76        }
77
78        @Override
79        public void onSave() {
80            if (sDebug) Slog.d(TAG, "OneTimeListener.onSave(): " + mDone);
81            if (mDone) {
82                return;
83            }
84            mDone = true;
85            mRealListener.onSave();
86        }
87
88        @Override
89        public void onCancel(IntentSender listener) {
90            if (sDebug) Slog.d(TAG, "OneTimeListener.onCancel(): " + mDone);
91            if (mDone) {
92                return;
93            }
94            mDone = true;
95            mRealListener.onCancel(listener);
96        }
97
98        @Override
99        public void onDestroy() {
100            if (sDebug) Slog.d(TAG, "OneTimeListener.onDestroy(): " + mDone);
101            if (mDone) {
102                return;
103            }
104            mDone = true;
105            mRealListener.onDestroy();
106        }
107    }
108
109    private final Handler mHandler = UiThread.getHandler();
110
111    private final @NonNull Dialog mDialog;
112
113    private final @NonNull OneTimeListener mListener;
114
115    private final @NonNull OverlayControl mOverlayControl;
116
117    private final CharSequence mTitle;
118    private final CharSequence mSubTitle;
119    private final PendingUi mPendingUi;
120
121    private boolean mDestroyed;
122
123    SaveUi(@NonNull Context context, @NonNull PendingUi pendingUi,
124           @NonNull CharSequence providerLabel, @NonNull SaveInfo info,
125           @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl,
126           @NonNull OnSaveListener listener) {
127        mPendingUi= pendingUi;
128        mListener = new OneTimeListener(listener);
129        mOverlayControl = overlayControl;
130
131        final LayoutInflater inflater = LayoutInflater.from(context);
132        final View view = inflater.inflate(R.layout.autofill_save, null);
133
134        final TextView titleView = view.findViewById(R.id.autofill_save_title);
135
136        final ArraySet<String> types = new ArraySet<>(3);
137        final int type = info.getType();
138
139        if ((type & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) {
140            types.add(context.getString(R.string.autofill_save_type_password));
141        }
142        if ((type & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) {
143            types.add(context.getString(R.string.autofill_save_type_address));
144        }
145        if ((type & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) {
146            types.add(context.getString(R.string.autofill_save_type_credit_card));
147        }
148        if ((type & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) {
149            types.add(context.getString(R.string.autofill_save_type_username));
150        }
151        if ((type & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) {
152            types.add(context.getString(R.string.autofill_save_type_email_address));
153        }
154
155        switch (types.size()) {
156            case 1:
157                mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type,
158                        types.valueAt(0), providerLabel), 0);
159                break;
160            case 2:
161                mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_2types,
162                        types.valueAt(0), types.valueAt(1), providerLabel), 0);
163                break;
164            case 3:
165                mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_3types,
166                        types.valueAt(0), types.valueAt(1), types.valueAt(2), providerLabel), 0);
167                break;
168            default:
169                // Use generic if more than 3 or invalid type (size 0).
170                mTitle = Html.fromHtml(
171                        context.getString(R.string.autofill_save_title, providerLabel), 0);
172        }
173
174        titleView.setText(mTitle);
175
176        ScrollView subtitleContainer = null;
177        final CustomDescription customDescription = info.getCustomDescription();
178        if (customDescription != null) {
179            mSubTitle = null;
180            if (sDebug) Slog.d(TAG, "Using custom description");
181
182            final RemoteViews presentation = customDescription.getPresentation(valueFinder);
183            if (presentation != null) {
184                final RemoteViews.OnClickHandler handler = new RemoteViews.OnClickHandler() {
185                    @Override
186                    public boolean onClickHandler(View view, PendingIntent pendingIntent,
187                            Intent intent) {
188                        // We need to hide the Save UI before launching the pending intent, and
189                        // restore back it once the activity is finished, and that's achieved by
190                        // adding a custom extra in the activity intent.
191                        if (pendingIntent != null) {
192                            if (intent == null) {
193                                Slog.w(TAG,
194                                        "remote view on custom description does not have intent");
195                                return false;
196                            }
197                            if (!pendingIntent.isActivity()) {
198                                Slog.w(TAG, "ignoring custom description pending intent that's not "
199                                        + "for an activity: " + pendingIntent);
200                                return false;
201                            }
202                            if (sVerbose) {
203                                Slog.v(TAG,
204                                        "Intercepting custom description intent: " + intent);
205                            }
206                            final IBinder token = mPendingUi.getToken();
207                            intent.putExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN, token);
208                            try {
209                                pendingUi.client.startIntentSender(pendingIntent.getIntentSender(),
210                                        intent);
211                                mPendingUi.setState(PendingUi.STATE_PENDING);
212                                if (sDebug) {
213                                    Slog.d(TAG, "hiding UI until restored with token " + token);
214                                }
215                                hide();
216                            } catch (RemoteException e) {
217                                Slog.w(TAG, "error triggering pending intent: " + intent);
218                                return false;
219                            }
220                        }
221                        return true;
222                    }
223                };
224
225                try {
226                    final View customSubtitleView = presentation.apply(context, null, handler);
227                    subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle);
228                    subtitleContainer.addView(customSubtitleView);
229                    subtitleContainer.setVisibility(View.VISIBLE);
230                } catch (Exception e) {
231                    Slog.e(TAG, "Could not inflate custom description. ", e);
232                }
233            } else {
234                Slog.w(TAG, "could not create remote presentation for custom title");
235            }
236        } else {
237            mSubTitle = info.getDescription();
238            if (mSubTitle != null) {
239                subtitleContainer = view.findViewById(R.id.autofill_save_custom_subtitle);
240                final TextView subtitleView = new TextView(context);
241                subtitleView.setText(mSubTitle);
242                subtitleContainer.addView(subtitleView,
243                        new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
244                                ViewGroup.LayoutParams.WRAP_CONTENT));
245                subtitleContainer.setVisibility(View.VISIBLE);
246            }
247            if (sDebug) Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle);
248        }
249
250        final TextView noButton = view.findViewById(R.id.autofill_save_no);
251        if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) {
252            noButton.setText(R.string.save_password_notnow);
253        } else {
254            noButton.setText(R.string.autofill_save_no);
255        }
256        final View.OnClickListener cancelListener =
257                (v) -> mListener.onCancel(info.getNegativeActionListener());
258        noButton.setOnClickListener(cancelListener);
259
260        final View yesButton = view.findViewById(R.id.autofill_save_yes);
261        yesButton.setOnClickListener((v) -> mListener.onSave());
262
263        mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel);
264        mDialog.setContentView(view);
265
266        // Dialog can be dismissed when touched outside.
267        mDialog.setOnDismissListener((d) -> mListener.onCancel(info.getNegativeActionListener()));
268
269        final Window window = mDialog.getWindow();
270        window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
271        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
272                | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
273                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
274                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
275        window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS);
276        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
277        window.setGravity(Gravity.BOTTOM | Gravity.CENTER);
278        window.setCloseOnTouchOutside(true);
279        final WindowManager.LayoutParams params = window.getAttributes();
280        params.width = WindowManager.LayoutParams.MATCH_PARENT;
281        params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title);
282        params.windowAnimations = R.style.AutofillSaveAnimation;
283
284        show();
285    }
286
287    /**
288     * Update the pending UI, if any.
289     *
290     * @param operation how to update it.
291     * @param token token associated with the pending UI - if it doesn't match the pending token,
292     * the operation will be ignored.
293     */
294    void onPendingUi(int operation, @NonNull IBinder token) {
295        if (!mPendingUi.matches(token)) {
296            Slog.w(TAG, "restore(" + operation + "): got token " + token + " instead of "
297                    + mPendingUi.getToken());
298            return;
299        }
300        switch (operation) {
301            case AutofillManager.PENDING_UI_OPERATION_RESTORE:
302                if (sDebug) Slog.d(TAG, "Restoring save dialog for " + token);
303                show();
304                break;
305            case AutofillManager.PENDING_UI_OPERATION_CANCEL:
306                if (sDebug) Slog.d(TAG, "Cancelling pending save dialog for " + token);
307                hide();
308                break;
309            default:
310                Slog.w(TAG, "restore(): invalid operation " + operation);
311        }
312        mPendingUi.setState(PendingUi.STATE_FINISHED);
313    }
314
315    private void show() {
316        Slog.i(TAG, "Showing save dialog: " + mTitle);
317        mDialog.show();
318        mOverlayControl.hideOverlays();
319   }
320
321    PendingUi hide() {
322        if (sVerbose) Slog.v(TAG, "Hiding save dialog.");
323        try {
324            mDialog.hide();
325        } finally {
326            mOverlayControl.showOverlays();
327        }
328        return mPendingUi;
329    }
330
331    void destroy() {
332        try {
333            if (sDebug) Slog.d(TAG, "destroy()");
334            throwIfDestroyed();
335            mListener.onDestroy();
336            mHandler.removeCallbacksAndMessages(mListener);
337            mDialog.dismiss();
338            mDestroyed = true;
339        } finally {
340            mOverlayControl.showOverlays();
341        }
342    }
343
344    private void throwIfDestroyed() {
345        if (mDestroyed) {
346            throw new IllegalStateException("cannot interact with a destroyed instance");
347        }
348    }
349
350    @Override
351    public String toString() {
352        return mTitle == null ? "NO TITLE" : mTitle.toString();
353    }
354
355    void dump(PrintWriter pw, String prefix) {
356        pw.print(prefix); pw.print("title: "); pw.println(mTitle);
357        pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle);
358        pw.print(prefix); pw.print("pendingUi: "); pw.println(mPendingUi);
359
360        final View view = mDialog.getWindow().getDecorView();
361        final int[] loc = view.getLocationOnScreen();
362        pw.print(prefix); pw.print("coordinates: ");
363            pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')');
364            pw.print('(');
365                pw.print(loc[0] + view.getWidth()); pw.print(',');
366                pw.print(loc[1] + view.getHeight());pw.println(')');
367        pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
368    }
369}
370