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.content.Context; 25import android.content.IntentSender; 26import android.os.Handler; 27import android.service.autofill.SaveInfo; 28import android.text.Html; 29import android.util.ArraySet; 30import android.util.Slog; 31import android.view.Gravity; 32import android.view.Window; 33import android.view.WindowManager; 34import android.widget.TextView; 35import android.view.LayoutInflater; 36import android.view.View; 37 38import com.android.internal.R; 39import com.android.server.UiThread; 40 41import java.io.PrintWriter; 42 43/** 44 * Autofill Save Prompt 45 */ 46final class SaveUi { 47 48 private static final String TAG = "AutofillSaveUi"; 49 50 public interface OnSaveListener { 51 void onSave(); 52 void onCancel(IntentSender listener); 53 void onDestroy(); 54 } 55 56 private class OneTimeListener implements OnSaveListener { 57 58 private final OnSaveListener mRealListener; 59 private boolean mDone; 60 61 OneTimeListener(OnSaveListener realListener) { 62 mRealListener = realListener; 63 } 64 65 @Override 66 public void onSave() { 67 if (sDebug) Slog.d(TAG, "OneTimeListener.onSave(): " + mDone); 68 if (mDone) { 69 return; 70 } 71 mDone = true; 72 mRealListener.onSave(); 73 } 74 75 @Override 76 public void onCancel(IntentSender listener) { 77 if (sDebug) Slog.d(TAG, "OneTimeListener.onCancel(): " + mDone); 78 if (mDone) { 79 return; 80 } 81 mDone = true; 82 mRealListener.onCancel(listener); 83 } 84 85 @Override 86 public void onDestroy() { 87 if (sDebug) Slog.d(TAG, "OneTimeListener.onDestroy(): " + mDone); 88 if (mDone) { 89 return; 90 } 91 mDone = true; 92 mRealListener.onDestroy(); 93 } 94 } 95 96 private final Handler mHandler = UiThread.getHandler(); 97 98 private final @NonNull Dialog mDialog; 99 100 private final @NonNull OneTimeListener mListener; 101 102 private final @NonNull OverlayControl mOverlayControl; 103 104 private final CharSequence mTitle; 105 private final CharSequence mSubTitle; 106 107 private boolean mDestroyed; 108 109 SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info, 110 @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener) { 111 mListener = new OneTimeListener(listener); 112 mOverlayControl = overlayControl; 113 114 final LayoutInflater inflater = LayoutInflater.from(context); 115 final View view = inflater.inflate(R.layout.autofill_save, null); 116 117 final TextView titleView = (TextView) view.findViewById(R.id.autofill_save_title); 118 119 final ArraySet<String> types = new ArraySet<>(3); 120 final int type = info.getType(); 121 122 if ((type & SaveInfo.SAVE_DATA_TYPE_PASSWORD) != 0) { 123 types.add(context.getString(R.string.autofill_save_type_password)); 124 } 125 if ((type & SaveInfo.SAVE_DATA_TYPE_ADDRESS) != 0) { 126 types.add(context.getString(R.string.autofill_save_type_address)); 127 } 128 if ((type & SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD) != 0) { 129 types.add(context.getString(R.string.autofill_save_type_credit_card)); 130 } 131 if ((type & SaveInfo.SAVE_DATA_TYPE_USERNAME) != 0) { 132 types.add(context.getString(R.string.autofill_save_type_username)); 133 } 134 if ((type & SaveInfo.SAVE_DATA_TYPE_EMAIL_ADDRESS) != 0) { 135 types.add(context.getString(R.string.autofill_save_type_email_address)); 136 } 137 138 switch (types.size()) { 139 case 1: 140 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type, 141 types.valueAt(0), providerLabel), 0); 142 break; 143 case 2: 144 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_2types, 145 types.valueAt(0), types.valueAt(1), providerLabel), 0); 146 break; 147 case 3: 148 mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_3types, 149 types.valueAt(0), types.valueAt(1), types.valueAt(2), providerLabel), 0); 150 break; 151 default: 152 // Use generic if more than 3 or invalid type (size 0). 153 mTitle = Html.fromHtml( 154 context.getString(R.string.autofill_save_title, providerLabel), 0); 155 } 156 157 titleView.setText(mTitle); 158 mSubTitle = info.getDescription(); 159 if (mSubTitle != null) { 160 final TextView subTitleView = (TextView) view.findViewById(R.id.autofill_save_subtitle); 161 subTitleView.setText(mSubTitle); 162 subTitleView.setVisibility(View.VISIBLE); 163 } 164 165 if (sDebug) { 166 Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); 167 } 168 169 final TextView noButton = view.findViewById(R.id.autofill_save_no); 170 if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { 171 noButton.setText(R.string.save_password_notnow); 172 } else { 173 noButton.setText(R.string.autofill_save_no); 174 } 175 View.OnClickListener cancelListener = 176 (v) -> mListener.onCancel(info.getNegativeActionListener()); 177 noButton.setOnClickListener(cancelListener); 178 179 final View yesButton = view.findViewById(R.id.autofill_save_yes); 180 yesButton.setOnClickListener((v) -> mListener.onSave()); 181 182 final View closeButton = view.findViewById(R.id.autofill_save_close); 183 closeButton.setOnClickListener(cancelListener); 184 185 mDialog = new Dialog(context, R.style.Theme_DeviceDefault_Light_Panel); 186 mDialog.setContentView(view); 187 188 final Window window = mDialog.getWindow(); 189 window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 190 window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 191 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 192 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 193 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); 194 window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS); 195 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); 196 window.setGravity(Gravity.BOTTOM | Gravity.CENTER); 197 window.setCloseOnTouchOutside(true); 198 final WindowManager.LayoutParams params = window.getAttributes(); 199 params.width = WindowManager.LayoutParams.MATCH_PARENT; 200 params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); 201 202 Slog.i(TAG, "Showing save dialog: " + mTitle); 203 mDialog.show(); 204 mOverlayControl.hideOverlays(); 205 } 206 207 void destroy() { 208 try { 209 if (sDebug) Slog.d(TAG, "destroy()"); 210 throwIfDestroyed(); 211 mListener.onDestroy(); 212 mHandler.removeCallbacksAndMessages(mListener); 213 if (sVerbose) Slog.v(TAG, "destroy(): dismissing dialog"); 214 mDialog.dismiss(); 215 mDestroyed = true; 216 } finally { 217 mOverlayControl.showOverlays(); 218 } 219 } 220 221 private void throwIfDestroyed() { 222 if (mDestroyed) { 223 throw new IllegalStateException("cannot interact with a destroyed instance"); 224 } 225 } 226 227 @Override 228 public String toString() { 229 return mTitle == null ? "NO TITLE" : mTitle.toString(); 230 } 231 232 void dump(PrintWriter pw, String prefix) { 233 pw.print(prefix); pw.print("title: "); pw.println(mTitle); 234 pw.print(prefix); pw.print("subtitle: "); pw.println(mSubTitle); 235 236 final View view = mDialog.getWindow().getDecorView(); 237 final int[] loc = view.getLocationOnScreen(); 238 pw.print(prefix); pw.print("coordinates: "); 239 pw.print('('); pw.print(loc[0]); pw.print(','); pw.print(loc[1]);pw.print(')'); 240 pw.print('('); 241 pw.print(loc[0] + view.getWidth()); pw.print(','); 242 pw.print(loc[1] + view.getHeight());pw.println(')'); 243 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 244 } 245} 246