• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 package com.android.settings.biometrics.combination;
17 
18 import static android.app.Activity.RESULT_OK;
19 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
20 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
21 
22 import static com.android.settings.password.ChooseLockPattern.RESULT_FINISHED;
23 
24 import android.app.Activity;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.hardware.biometrics.SensorProperties;
28 import android.hardware.face.FaceManager;
29 import android.hardware.face.FaceSensorPropertiesInternal;
30 import android.hardware.fingerprint.FingerprintManager;
31 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import androidx.activity.result.ActivityResult;
38 import androidx.activity.result.ActivityResultLauncher;
39 import androidx.activity.result.contract.ActivityResultContracts;
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.StringRes;
43 import androidx.annotation.VisibleForTesting;
44 import androidx.preference.Preference;
45 
46 import com.android.settings.R;
47 import com.android.settings.Utils;
48 import com.android.settings.biometrics.BiometricEnrollBase;
49 import com.android.settings.biometrics.BiometricStatusPreferenceController;
50 import com.android.settings.biometrics.BiometricUtils;
51 import com.android.settings.biometrics.BiometricsSplitScreenDialog;
52 import com.android.settings.core.SettingsBaseActivity;
53 import com.android.settings.dashboard.DashboardFragment;
54 import com.android.settings.password.ChooseLockGeneric;
55 import com.android.settings.password.ChooseLockSettingsHelper;
56 import com.android.settingslib.activityembedding.ActivityEmbeddingUtils;
57 import com.android.settingslib.core.AbstractPreferenceController;
58 import com.android.settingslib.transition.SettingsTransitionHelper;
59 
60 import java.util.Collection;
61 import java.util.List;
62 
63 /**
64  * Base fragment with the confirming credential functionality for combined biometrics settings.
65  */
66 public abstract class BiometricsSettingsBase extends DashboardFragment {
67 
68     @VisibleForTesting
69     static final int CONFIRM_REQUEST = 2001;
70     private static final int CHOOSE_LOCK_REQUEST = 2002;
71     protected static final int ACTIVE_UNLOCK_REQUEST = 2003;
72 
73     private static final String SAVE_STATE_CONFIRM_CREDETIAL = "confirm_credential";
74     private static final String DO_NOT_FINISH_ACTIVITY = "do_not_finish_activity";
75     @VisibleForTesting
76     static final String RETRY_PREFERENCE_KEY = "retry_preference_key";
77     @VisibleForTesting
78     static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle";
79 
80     protected int mUserId;
81     protected long mGkPwHandle;
82     private boolean mConfirmCredential;
83     @Nullable private FaceManager mFaceManager;
84     @Nullable private FingerprintManager mFingerprintManager;
85     // Do not finish() if choosing/confirming credential, showing fp/face settings, or launching
86     // active unlock
87     protected boolean mDoNotFinishActivity;
88     @Nullable private String mRetryPreferenceKey = null;
89     @Nullable private Bundle mRetryPreferenceExtra = null;
90 
91     private final ActivityResultLauncher<Intent> mFaceOrFingerprintPreferenceLauncher =
92             registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
93                     this::onFaceOrFingerprintPreferenceResult);
94 
onFaceOrFingerprintPreferenceResult(@ullable ActivityResult result)95     private void onFaceOrFingerprintPreferenceResult(@Nullable ActivityResult result) {
96         if (result != null && result.getResultCode() == BiometricEnrollBase.RESULT_TIMEOUT) {
97             // When "Face Unlock" or "Fingerprint Unlock" is closed due to entering onStop(),
98             // "Face & Fingerprint Unlock" shall also close itself and back to "Security" page.
99             finish();
100         }
101     }
102 
103     @Override
onAttach(Context context)104     public void onAttach(Context context) {
105         super.onAttach(context);
106         mUserId = getActivity().getIntent().getIntExtra(Intent.EXTRA_USER_ID,
107                 UserHandle.myUserId());
108     }
109 
110     @Override
onCreate(Bundle savedInstanceState)111     public void onCreate(Bundle savedInstanceState) {
112         super.onCreate(savedInstanceState);
113         mFaceManager = Utils.getFaceManagerOrNull(getActivity());
114         mFingerprintManager = Utils.getFingerprintManagerOrNull(getActivity());
115 
116         if (BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
117             mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent());
118         }
119 
120         if (savedInstanceState != null) {
121             mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL);
122             mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY);
123             mRetryPreferenceKey = savedInstanceState.getString(RETRY_PREFERENCE_KEY);
124             mRetryPreferenceExtra = savedInstanceState.getBundle(RETRY_PREFERENCE_BUNDLE);
125             if (savedInstanceState.containsKey(
126                     ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE)) {
127                 mGkPwHandle = savedInstanceState.getLong(
128                         ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE);
129             }
130         }
131 
132         if (mGkPwHandle == 0L && !mConfirmCredential) {
133             mConfirmCredential = true;
134             launchChooseOrConfirmLock();
135         }
136 
137         updateUnlockPhonePreferenceSummary();
138 
139         final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey());
140         if (useInAppsPreference != null) {
141             useInAppsPreference.setSummary(getUseClass2BiometricSummary());
142         }
143     }
144 
145     @Override
onResume()146     public void onResume() {
147         super.onResume();
148         if (!mConfirmCredential) {
149             mDoNotFinishActivity = false;
150         }
151     }
152 
153     @Override
onStop()154     public void onStop() {
155         super.onStop();
156         if (!getActivity().isChangingConfigurations() && !mDoNotFinishActivity) {
157             BiometricUtils.removeGatekeeperPasswordHandle(getActivity(), mGkPwHandle);
158             getActivity().finish();
159         }
160     }
161 
onRetryPreferenceTreeClick(Preference preference, final boolean retry)162     protected boolean onRetryPreferenceTreeClick(Preference preference, final boolean retry) {
163         final String key = preference.getKey();
164         final Context context = requireActivity().getApplicationContext();
165 
166         // Generate challenge (and request LSS to create a HAT) each time the preference is clicked,
167         // since FingerprintSettings and FaceSettings revoke the challenge when finishing.
168         if (getFacePreferenceKey().equals(key)) {
169             mDoNotFinishActivity = true;
170 
171             //  If it's split mode and there is no enrolled face, show the dialog. (if there is
172             //  enrolled face, FaceSettingsEnrollButtonPreferenceController#onClick will handle
173             //  the dialog)
174             if (getActivity().isInMultiWindowMode() && !ActivityEmbeddingUtils.isActivityEmbedded(
175                     getActivity()) && !mFaceManager.hasEnrolledTemplates(mUserId)) {
176                 BiometricsSplitScreenDialog.newInstance(TYPE_FACE).show(
177                         getActivity().getSupportFragmentManager(),
178                         BiometricsSplitScreenDialog.class.getName());
179                 return true;
180             }
181 
182             mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
183                 final Activity activity = getActivity();
184                 if (activity == null || activity.isFinishing()) {
185                     Log.e(getLogTag(), "Stop during generating face unlock challenge"
186                             + " because activity is null or finishing");
187                     return;
188                 }
189                 try {
190                     final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
191                             challenge);
192                     final Bundle extras = preference.getExtras();
193                     extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
194                     extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId);
195                     extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge);
196                     onFaceOrFingerprintPreferenceTreeClick(preference);
197                 } catch (IllegalStateException e) {
198                     if (retry) {
199                         mRetryPreferenceKey = preference.getKey();
200                         mRetryPreferenceExtra = preference.getExtras();
201                         mConfirmCredential = true;
202                         launchChooseOrConfirmLock();
203                     } else {
204                         Log.e(getLogTag(), "face generateChallenge fail", e);
205                         mDoNotFinishActivity = false;
206                     }
207                 }
208             });
209             return true;
210         } else if (getFingerprintPreferenceKey().equals(key)) {
211             mDoNotFinishActivity = true;
212 
213             //  If it's split mode and there is no enrolled fingerprint, show the dialog. (if
214             //  there is enrolled fingerprint, FingerprintSettingsFragment#onPreferenceTreeClick
215             //  will handle the dialog)
216             if (getActivity().isInMultiWindowMode() && !ActivityEmbeddingUtils.isActivityEmbedded(
217                     getActivity()) && !mFingerprintManager.hasEnrolledFingerprints(mUserId)) {
218                 BiometricsSplitScreenDialog.newInstance(TYPE_FINGERPRINT).show(
219                         getActivity().getSupportFragmentManager(),
220                         BiometricsSplitScreenDialog.class.getName());
221                 return true;
222             }
223 
224             mFingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
225                 final Activity activity = getActivity();
226                 if (activity == null || activity.isFinishing()) {
227                     Log.e(getLogTag(), "Stop during generating fingerprint challenge"
228                             + " because activity is null or finishing");
229                     return;
230                 }
231                 try {
232                     final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
233                             challenge);
234                     final Bundle extras = preference.getExtras();
235                     extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
236                     extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge);
237                     onFaceOrFingerprintPreferenceTreeClick(preference);
238                 } catch (IllegalStateException e) {
239                     if (retry) {
240                         mRetryPreferenceKey = preference.getKey();
241                         mRetryPreferenceExtra = preference.getExtras();
242                         mConfirmCredential = true;
243                         launchChooseOrConfirmLock();
244                     } else {
245                         Log.e(getLogTag(), "fingerprint generateChallenge fail", e);
246                         mDoNotFinishActivity = false;
247                     }
248                 }
249             });
250             return true;
251         }
252         return false;
253     }
254 
255     @VisibleForTesting
requestGatekeeperHat(@onNull Context context, long gkPwHandle, int userId, long challenge)256     protected byte[] requestGatekeeperHat(@NonNull Context context, long gkPwHandle, int userId,
257             long challenge) {
258         return BiometricUtils.requestGatekeeperHat(context, gkPwHandle, userId, challenge);
259     }
260 
261     /**
262      * Handle preference tree click action for "Face Unlock" or "Fingerprint Unlock" with a launcher
263      * because "Face & Fingerprint Unlock" has to close itself when it gets a specific activity
264      * error code.
265      *
266      * @param preference "Face Unlock" or "Fingerprint Unlock" preference.
267      */
onFaceOrFingerprintPreferenceTreeClick(@onNull Preference preference)268     private void onFaceOrFingerprintPreferenceTreeClick(@NonNull Preference preference) {
269         Collection<List<AbstractPreferenceController>> controllers = getPreferenceControllers();
270         for (List<AbstractPreferenceController> controllerList : controllers) {
271             for (AbstractPreferenceController controller : controllerList) {
272                 if (controller instanceof BiometricStatusPreferenceController) {
273                     final BiometricStatusPreferenceController biometricController =
274                             (BiometricStatusPreferenceController) controller;
275                     if (biometricController.setPreferenceTreeClickLauncher(preference,
276                             mFaceOrFingerprintPreferenceLauncher)) {
277                         if (biometricController.handlePreferenceTreeClick(preference)) {
278                             writePreferenceClickMetric(preference);
279                         }
280                         biometricController.setPreferenceTreeClickLauncher(preference, null);
281                         return;
282                     }
283                 }
284             }
285         }
286     }
287 
288     @Override
onPreferenceTreeClick(Preference preference)289     public boolean onPreferenceTreeClick(Preference preference) {
290         return onRetryPreferenceTreeClick(preference, true)
291                 || super.onPreferenceTreeClick(preference);
292     }
293 
retryPreferenceKey(@onNull String key, @Nullable Bundle extras)294     private void retryPreferenceKey(@NonNull String key, @Nullable Bundle extras) {
295         final Preference preference = findPreference(key);
296         if (preference == null) {
297             Log.w(getLogTag(), ".retryPreferenceKey, fail to find " + key);
298             return;
299         }
300 
301         if (extras != null) {
302             preference.getExtras().putAll(extras);
303         }
304         onRetryPreferenceTreeClick(preference, false);
305     }
306 
307     @Override
onSaveInstanceState(Bundle outState)308     public void onSaveInstanceState(Bundle outState) {
309         super.onSaveInstanceState(outState);
310         outState.putBoolean(SAVE_STATE_CONFIRM_CREDETIAL, mConfirmCredential);
311         outState.putBoolean(DO_NOT_FINISH_ACTIVITY, mDoNotFinishActivity);
312         if (mGkPwHandle != 0L) {
313             outState.putLong(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, mGkPwHandle);
314         }
315         if (!TextUtils.isEmpty(mRetryPreferenceKey)) {
316             outState.putString(RETRY_PREFERENCE_KEY, mRetryPreferenceKey);
317             outState.putBundle(RETRY_PREFERENCE_BUNDLE, mRetryPreferenceExtra);
318         }
319     }
320 
321     @Override
onActivityResult(int requestCode, int resultCode, @Nullable Intent data)322     public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
323         super.onActivityResult(requestCode, resultCode, data);
324         if (requestCode == CONFIRM_REQUEST || requestCode == CHOOSE_LOCK_REQUEST) {
325             mConfirmCredential = false;
326             mDoNotFinishActivity = false;
327             if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) {
328                 if (BiometricUtils.containsGatekeeperPasswordHandle(data)) {
329                     mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
330                     if (!TextUtils.isEmpty(mRetryPreferenceKey)) {
331                         getActivity().overridePendingTransition(R.anim.sud_slide_next_in,
332                                 R.anim.sud_slide_next_out);
333                         retryPreferenceKey(mRetryPreferenceKey, mRetryPreferenceExtra);
334                     }
335                 } else {
336                     Log.d(getLogTag(), "Data null or GK PW missing.");
337                     finish();
338                 }
339             } else {
340                 Log.d(getLogTag(), "Password not confirmed.");
341                 finish();
342             }
343             mRetryPreferenceKey = null;
344             mRetryPreferenceExtra = null;
345         }
346     }
347 
348     /**
349      * Get the preference key of face for passing through credential data to face settings.
350      */
getFacePreferenceKey()351     public abstract String getFacePreferenceKey();
352 
353     /**
354      * Get the preference key of face for passing through credential data to face settings.
355      */
getFingerprintPreferenceKey()356     public abstract String getFingerprintPreferenceKey();
357 
358     /**
359      * @return The preference key of the "Unlock your phone" setting toggle.
360      */
getUnlockPhonePreferenceKey()361     public abstract String getUnlockPhonePreferenceKey();
362 
363     /**
364      * @return The preference key of the "Verify it's you in apps" setting toggle.
365      */
getUseInAppsPreferenceKey()366     public abstract String getUseInAppsPreferenceKey();
367 
368     @VisibleForTesting
launchChooseOrConfirmLock()369     protected void launchChooseOrConfirmLock() {
370         final ChooseLockSettingsHelper.Builder builder =
371                 new ChooseLockSettingsHelper.Builder(getActivity(), this)
372                         .setRequestCode(CONFIRM_REQUEST)
373                         .setTitle(getString(R.string.security_settings_biometric_preference_title))
374                         .setRequestGatekeeperPasswordHandle(true)
375                         .setForegroundOnly(true)
376                         .setReturnCredentials(true);
377         if (mUserId != UserHandle.USER_NULL) {
378             builder.setUserId(mUserId);
379         }
380         mDoNotFinishActivity = true;
381         final boolean launched = builder.show();
382 
383         if (!launched) {
384             Intent intent = BiometricUtils.getChooseLockIntent(getActivity(), getIntent());
385             intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
386                     true);
387             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
388             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, true);
389             intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
390                     SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE);
391 
392             if (mUserId != UserHandle.USER_NULL) {
393                 intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
394             }
395             startActivityForResult(intent, CHOOSE_LOCK_REQUEST);
396         }
397     }
398 
updateUnlockPhonePreferenceSummary()399     protected void updateUnlockPhonePreferenceSummary() {
400         final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey());
401         if (unlockPhonePreference != null) {
402             unlockPhonePreference.setSummary(getUseAnyBiometricSummary());
403         }
404     }
405 
406     @NonNull
getUseAnyBiometricSummary()407     protected String getUseAnyBiometricSummary() {
408         boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected();
409         boolean isFingerprintAllowed =
410                 mFingerprintManager != null && mFingerprintManager.isHardwareDetected();
411 
412         @StringRes final int resId = getUseBiometricSummaryRes(isFaceAllowed, isFingerprintAllowed);
413         return resId == 0 ? "" : getString(resId);
414     }
415 
getUserId()416     protected int getUserId() {
417         return mUserId;
418     }
419 
getGkPwHandle()420     protected long getGkPwHandle() {
421         return mGkPwHandle;
422     }
423 
424     @NonNull
getUseClass2BiometricSummary()425     private String getUseClass2BiometricSummary() {
426         boolean isFaceAllowed = false;
427         if (mFaceManager != null) {
428             for (final FaceSensorPropertiesInternal sensorProps
429                     : mFaceManager.getSensorPropertiesInternal()) {
430                 if (sensorProps.sensorStrength == SensorProperties.STRENGTH_WEAK
431                         || sensorProps.sensorStrength == SensorProperties.STRENGTH_STRONG) {
432                     isFaceAllowed = true;
433                     break;
434                 }
435             }
436         }
437 
438         boolean isFingerprintAllowed = false;
439         if (mFingerprintManager != null) {
440             for (final FingerprintSensorPropertiesInternal sensorProps
441                     : mFingerprintManager.getSensorPropertiesInternal()) {
442                 if (sensorProps.sensorStrength == SensorProperties.STRENGTH_WEAK
443                         || sensorProps.sensorStrength == SensorProperties.STRENGTH_STRONG) {
444                     isFingerprintAllowed = true;
445                     break;
446                 }
447             }
448         }
449 
450         @StringRes final int resId = getUseBiometricSummaryRes(isFaceAllowed, isFingerprintAllowed);
451         return resId == 0 ? "" : getString(resId);
452     }
453 
454     @StringRes
getUseBiometricSummaryRes(boolean isFaceAllowed, boolean isFingerprintAllowed)455     private static int getUseBiometricSummaryRes(boolean isFaceAllowed,
456             boolean isFingerprintAllowed) {
457 
458         if (isFaceAllowed && isFingerprintAllowed) {
459             return R.string.biometric_settings_use_face_or_fingerprint_preference_summary;
460         } else if (isFaceAllowed) {
461             return R.string.biometric_settings_use_face_preference_summary;
462         } else if (isFingerprintAllowed) {
463             return R.string.biometric_settings_use_fingerprint_preference_summary;
464         } else {
465             return 0;
466         }
467     }
468 }
469