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