/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.autofill.ui; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; import android.annotation.NonNull; import android.app.Dialog; import android.content.Context; import android.content.IntentSender; import android.os.Handler; import android.service.autofill.SaveInfo; import android.text.Html; import android.util.ArraySet; import android.util.Slog; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; import android.widget.TextView; import android.view.LayoutInflater; import android.view.View; import com.android.internal.R; import com.android.server.UiThread; import java.io.PrintWriter; /** * Autofill Save Prompt */ final class SaveUi { private static final String TAG = "AutofillSaveUi"; public interface OnSaveListener { void onSave(); void onCancel(IntentSender listener); void onDestroy(); } private class OneTimeListener implements OnSaveListener { private final OnSaveListener mRealListener; private boolean mDone; OneTimeListener(OnSaveListener realListener) { mRealListener = realListener; } @Override public void onSave() { if (sDebug) Slog.d(TAG, "OneTimeListener.onSave(): " + mDone); if (mDone) { return; } mDone = true; mRealListener.onSave(); } @Override public void onCancel(IntentSender listener) { if (sDebug) Slog.d(TAG, "OneTimeListener.onCancel(): " + mDone); if (mDone) { return; } mDone = true; mRealListener.onCancel(listener); } @Override public void onDestroy() { if (sDebug) Slog.d(TAG, "OneTimeListener.onDestroy(): " + mDone); if (mDone) { return; } mDone = true; mRealListener.onDestroy(); } } private final Handler mHandler = UiThread.getHandler(); private final @NonNull Dialog mDialog; private final @NonNull OneTimeListener mListener; private final @NonNull OverlayControl mOverlayControl; private final CharSequence mTitle; private final CharSequence mSubTitle; private boolean mDestroyed; SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info, @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener) { mListener = new OneTimeListener(listener); mOverlayControl = overlayControl; final LayoutInflater inflater = LayoutInflater.from(context); final View view = inflater.inflate(R.layout.autofill_save, null); final TextView titleView = (TextView) view.findViewById(R.id.autofill_save_title); final ArraySet types = new ArraySet<>(3); final int type = info.getType(); if ((type & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) { types.add(context.getString(R.string.autofill_save_type_password)); } if ((type & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) { types.add(context.getString(R.string.autofill_save_type_address)); } if ((type & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) { types.add(context.getString(R.string.autofill_save_type_credit_card)); } if ((type & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) { types.add(context.getString(R.string.autofill_save_type_username)); } if ((type & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) { types.add(context.getString(R.string.autofill_save_type_email_address)); } switch (types.size()) { case 1: mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type, types.valueAt(0), providerLabel), 0); break; case 2: mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_2types, types.valueAt(0), types.valueAt(1), providerLabel), 0); break; case 3: mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_3types, types.valueAt(0), types.valueAt(1), types.valueAt(2), providerLabel), 0); break; default: // Use generic if more than 3 or invalid type (size 0). mTitle = Html.fromHtml( context.getString(R.string.autofill_save_title, providerLabel), 0); } titleView.setText(mTitle); mSubTitle = info.getDescription(); if (mSubTitle != null) { final TextView subTitleView = (TextView) view.findViewById(R.id.autofill_save_subtitle); subTitleView.setText(mSubTitle); subTitleView.setVisibility(View.VISIBLE); } if (sDebug) { Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); } final TextView noButton = view.findViewById(R.id.autofill_save_no); if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { noButton.setText(R.string.save_password_notnow); } else { noButton.setText(R.string.autofill_save_no); } View.OnClickListener cancelListener = (v) -> mListener.onCancel(info.getNegativeActionListener()); noButton.setOnClickListener(cancelListener); final View yesButton = view.findViewById(R.id.autofill_save_yes); yesButton.setOnClickListener((v) -> mListener.onSave()); final View closeButton = view.findViewById(R.id.autofill_save_close); closeButton.setOnClickListener(cancelListener); mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel); mDialog.setContentView(view); final Window window = mDialog.getWindow(); window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS); window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); window.setGravity(Gravity.BOTTOM | Gravity.CENTER); window.setCloseOnTouchOutside(true); final WindowManager.LayoutParams params = window.getAttributes(); params.width = WindowManager.LayoutParams.MATCH_PARENT; params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); Slog.i(TAG, "Showing save dialog: " + mTitle); mDialog.show(); mOverlayControl.hideOverlays(); } void destroy() { try { if (sDebug) Slog.d(TAG, "destroy()"); throwIfDestroyed(); mListener.onDestroy(); mHandler.removeCallbacksAndMessages(mListener); if (sVerbose) Slog.v(TAG, "destroy(): dismissing dialog"); mDialog.dismiss(); mDestroyed = true; } finally { mOverlayControl.showOverlays(); } } private void throwIfDestroyed() { if (mDestroyed) { throw new IllegalStateException("cannot interact with a destroyed instance"); } } @Override public String toString() { return mTitle == null ? "NO TITLE" : mTitle.toString(); } void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("title: "); pw.println(mTitle); pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); final View view = mDialog.getWindow().getDecorView(); final int[] loc = view.getLocationOnScreen(); pw.print(prefix); pw.print("coordinates: "); pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')'); pw.print('('); pw.print(loc[0] + view.getWidth()); pw.print(','); pw.print(loc[1] + view.getHeight());pw.println(')'); pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); } }