1 /* 2 * Copyright (C) 2018 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.settings.biometrics.face; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.settings.SettingsEnums; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.pm.PackageManager; 25 import android.hardware.face.Face; 26 import android.hardware.face.FaceManager; 27 import android.os.Bundle; 28 import android.util.Log; 29 import android.view.View; 30 import android.widget.Button; 31 import android.widget.Toast; 32 import android.window.OnBackInvokedCallback; 33 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.preference.Preference; 37 38 import com.android.settings.R; 39 import com.android.settings.SettingsActivity; 40 import com.android.settings.biometrics.BiometricUtils; 41 import com.android.settings.core.BasePreferenceController; 42 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 43 import com.android.settings.overlay.FeatureFactory; 44 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 45 import com.android.settingslib.widget.LayoutPreference; 46 47 import com.google.android.setupdesign.util.ButtonStyler; 48 import com.google.android.setupdesign.util.PartnerStyleHelper; 49 50 import java.util.List; 51 52 /** 53 * Controller for the remove button. This assumes that there is only a single face enrolled. The UI 54 * will likely change if multiple enrollments are allowed/supported. 55 */ 56 public class FaceSettingsRemoveButtonPreferenceController extends BasePreferenceController 57 implements View.OnClickListener { 58 59 private static final String TAG = "FaceSettings/Remove"; 60 static final String KEY = "security_settings_face_delete_faces_container"; 61 62 public static class ConfirmRemoveDialog extends InstrumentedDialogFragment 63 implements OnBackInvokedCallback { 64 private static final String KEY_IS_CONVENIENCE = "is_convenience"; 65 private DialogInterface.OnClickListener mOnClickListener; 66 @Nullable 67 private AlertDialog mDialog = null; 68 @Nullable 69 private Preference mFaceUnlockPreference = null; 70 71 /** Returns the new instance of the class */ newInstance(boolean isConvenience)72 public static ConfirmRemoveDialog newInstance(boolean isConvenience) { 73 final ConfirmRemoveDialog dialog = new ConfirmRemoveDialog(); 74 final Bundle args = new Bundle(); 75 args.putBoolean(KEY_IS_CONVENIENCE, isConvenience); 76 dialog.setArguments(args); 77 return dialog; 78 } 79 80 @Override getMetricsCategory()81 public int getMetricsCategory() { 82 return SettingsEnums.DIALOG_FACE_REMOVE; 83 } 84 85 @Override onCreateDialog(Bundle savedInstanceState)86 public Dialog onCreateDialog(Bundle savedInstanceState) { 87 boolean isConvenience = getArguments().getBoolean(KEY_IS_CONVENIENCE); 88 89 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 90 91 final PackageManager pm = getContext().getPackageManager(); 92 final boolean hasFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); 93 final int dialogMessageRes; 94 95 if (hasFingerprint) { 96 dialogMessageRes = isConvenience 97 ? R.string.security_settings_face_remove_dialog_details_fingerprint_conv 98 : R.string.security_settings_face_remove_dialog_details_fingerprint; 99 } else { 100 dialogMessageRes = isConvenience 101 ? R.string.security_settings_face_settings_remove_dialog_details_convenience 102 : R.string.security_settings_face_settings_remove_dialog_details; 103 } 104 105 builder.setTitle(R.string.security_settings_face_settings_remove_dialog_title) 106 .setMessage(dialogMessageRes) 107 .setPositiveButton(R.string.delete, mOnClickListener) 108 .setNegativeButton(R.string.cancel, mOnClickListener); 109 mDialog = builder.create(); 110 mDialog.setCanceledOnTouchOutside(false); 111 mDialog.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(0, this); 112 return mDialog; 113 } 114 setOnClickListener(DialogInterface.OnClickListener listener)115 public void setOnClickListener(DialogInterface.OnClickListener listener) { 116 mOnClickListener = listener; 117 } 118 setPreference(@ullable Preference preference)119 public void setPreference(@Nullable Preference preference) { 120 mFaceUnlockPreference = preference; 121 } 122 unregisterOnBackInvokedCallback()123 public void unregisterOnBackInvokedCallback() { 124 if (mDialog != null) { 125 mDialog.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(this); 126 } 127 } 128 129 @Override onBackInvoked()130 public void onBackInvoked() { 131 if (mDialog != null) { 132 mDialog.cancel(); 133 } 134 unregisterOnBackInvokedCallback(); 135 136 if (mFaceUnlockPreference != null) { 137 final Button removeButton = ((LayoutPreference) mFaceUnlockPreference) 138 .findViewById(R.id.security_settings_face_settings_remove_button); 139 if (removeButton != null) { 140 removeButton.setEnabled(true); 141 } 142 } 143 } 144 } 145 146 interface Listener { onRemoved()147 void onRemoved(); 148 } 149 150 private Preference mPreference; 151 private Button mButton; 152 private Listener mListener; 153 private SettingsActivity mActivity; 154 private int mUserId; 155 @VisibleForTesting 156 boolean mRemoving; 157 158 private final MetricsFeatureProvider mMetricsFeatureProvider; 159 private final Context mContext; 160 private final FaceManager mFaceManager; 161 private final FaceUpdater mFaceUpdater; 162 private final FaceManager.RemovalCallback mRemovalCallback = new FaceManager.RemovalCallback() { 163 @Override 164 public void onRemovalError(Face face, int errMsgId, CharSequence errString) { 165 Log.e(TAG, "Unable to remove face: " + face.getBiometricId() 166 + " error: " + errMsgId + " " + errString); 167 Toast.makeText(mContext, errString, Toast.LENGTH_SHORT).show(); 168 mRemoving = false; 169 } 170 171 @Override 172 public void onRemovalSucceeded(Face face, int remaining) { 173 if (remaining == 0) { 174 final List<Face> faces = mFaceManager.getEnrolledFaces(mUserId); 175 if (!faces.isEmpty()) { 176 mButton.setEnabled(true); 177 } else { 178 mRemoving = false; 179 mListener.onRemoved(); 180 } 181 } else { 182 Log.v(TAG, "Remaining: " + remaining); 183 } 184 } 185 }; 186 187 private final DialogInterface.OnClickListener mOnConfirmDialogClickListener 188 = new DialogInterface.OnClickListener() { 189 @Override 190 public void onClick(DialogInterface dialog, int which) { 191 if (which == DialogInterface.BUTTON_POSITIVE) { 192 mButton.setEnabled(false); 193 final List<Face> faces = mFaceManager.getEnrolledFaces(mUserId); 194 if (faces.isEmpty()) { 195 Log.e(TAG, "No faces"); 196 return; 197 } 198 if (faces.size() > 1) { 199 Log.e(TAG, "Multiple enrollments: " + faces.size()); 200 } 201 202 // Remove the first/only face 203 mFaceUpdater.remove(faces.get(0), mUserId, mRemovalCallback); 204 } else { 205 mButton.setEnabled(true); 206 mRemoving = false; 207 } 208 209 final ConfirmRemoveDialog removeDialog = 210 (ConfirmRemoveDialog) mActivity.getSupportFragmentManager() 211 .findFragmentByTag(ConfirmRemoveDialog.class.getName()); 212 if (removeDialog != null) { 213 removeDialog.unregisterOnBackInvokedCallback(); 214 } 215 } 216 }; 217 FaceSettingsRemoveButtonPreferenceController(Context context, String preferenceKey)218 public FaceSettingsRemoveButtonPreferenceController(Context context, String preferenceKey) { 219 super(context, preferenceKey); 220 mContext = context; 221 mFaceManager = context.getSystemService(FaceManager.class); 222 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 223 mFaceUpdater = new FaceUpdater(context, mFaceManager); 224 } 225 FaceSettingsRemoveButtonPreferenceController(Context context)226 public FaceSettingsRemoveButtonPreferenceController(Context context) { 227 this(context, KEY); 228 } 229 setUserId(int userId)230 public void setUserId(int userId) { 231 mUserId = userId; 232 } 233 234 @Override updateState(Preference preference)235 public void updateState(Preference preference) { 236 super.updateState(preference); 237 238 mPreference = preference; 239 mButton = ((LayoutPreference) preference) 240 .findViewById(R.id.security_settings_face_settings_remove_button); 241 242 if (PartnerStyleHelper.shouldApplyPartnerResource(mButton)) { 243 ButtonStyler.applyPartnerCustomizationPrimaryButtonStyle(mContext, mButton); 244 } 245 246 mButton.setOnClickListener(this); 247 248 // If there is already a ConfirmRemoveDialog showing, reset the listener since the 249 // controller has been recreated. 250 ConfirmRemoveDialog removeDialog = 251 (ConfirmRemoveDialog) mActivity.getSupportFragmentManager() 252 .findFragmentByTag(ConfirmRemoveDialog.class.getName()); 253 if (removeDialog != null) { 254 removeDialog.setPreference(mPreference); 255 mRemoving = true; 256 removeDialog.setOnClickListener(mOnConfirmDialogClickListener); 257 } 258 259 if (!FaceSettings.isFaceHardwareDetected(mContext)) { 260 mButton.setEnabled(false); 261 } else { 262 mButton.setEnabled(!mRemoving); 263 } 264 } 265 266 @Override getAvailabilityStatus()267 public int getAvailabilityStatus() { 268 return AVAILABLE; 269 } 270 271 @Override getPreferenceKey()272 public String getPreferenceKey() { 273 return KEY; 274 } 275 276 @Override onClick(View v)277 public void onClick(View v) { 278 if (v == mButton) { 279 mMetricsFeatureProvider.logClickedPreference(mPreference, getMetricsCategory()); 280 mRemoving = true; 281 ConfirmRemoveDialog confirmRemoveDialog = 282 ConfirmRemoveDialog.newInstance(BiometricUtils.isConvenience(mFaceManager)); 283 confirmRemoveDialog.setOnClickListener(mOnConfirmDialogClickListener); 284 confirmRemoveDialog.show(mActivity.getSupportFragmentManager(), 285 ConfirmRemoveDialog.class.getName()); 286 } 287 } 288 setListener(Listener listener)289 public void setListener(Listener listener) { 290 mListener = listener; 291 } 292 setActivity(SettingsActivity activity)293 public void setActivity(SettingsActivity activity) { 294 mActivity = activity; 295 } 296 } 297