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