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