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