• 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 
17 package com.android.settings.biometrics.face;
18 
19 import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_UNLOCK_DISABLED;
20 
21 import static com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException;
22 
23 import android.app.admin.DevicePolicyManager;
24 import android.app.settings.SettingsEnums;
25 import android.content.Intent;
26 import android.content.res.Configuration;
27 import android.hardware.SensorPrivacyManager;
28 import android.hardware.biometrics.BiometricAuthenticator;
29 import android.hardware.face.FaceManager;
30 import android.os.Bundle;
31 import android.os.UserHandle;
32 import android.text.Html;
33 import android.text.method.LinkMovementMethod;
34 import android.util.Log;
35 import android.view.View;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.TextView;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.StringRes;
43 import androidx.annotation.VisibleForTesting;
44 
45 import com.android.settings.R;
46 import com.android.settings.Settings;
47 import com.android.settings.Utils;
48 import com.android.settings.biometrics.BiometricEnrollActivity;
49 import com.android.settings.biometrics.BiometricEnrollIntroduction;
50 import com.android.settings.biometrics.BiometricUtils;
51 import com.android.settings.biometrics.MultiBiometricEnrollHelper;
52 import com.android.settings.password.ChooseLockSettingsHelper;
53 import com.android.settings.password.SetupSkipDialog;
54 import com.android.settings.utils.SensorPrivacyManagerHelper;
55 import com.android.settingslib.RestrictedLockUtilsInternal;
56 import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
57 import com.android.systemui.unfold.updates.FoldProvider;
58 
59 import com.google.android.setupcompat.template.FooterButton;
60 import com.google.android.setupcompat.util.WizardManagerHelper;
61 import com.google.android.setupdesign.span.LinkSpan;
62 
63 /**
64  * Provides introductory info about face unlock and prompts the user to agree before starting face
65  * enrollment.
66  */
67 public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
68     private static final String TAG = "FaceEnrollIntroduction";
69 
70     private FaceManager mFaceManager;
71     @Nullable private FooterButton mPrimaryFooterButton;
72     @Nullable private FooterButton mSecondaryFooterButton;
73     @Nullable private SensorPrivacyManager mSensorPrivacyManager;
74 
75     @Override
onCancelButtonClick(View view)76     protected void onCancelButtonClick(View view) {
77         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
78                 "cancel")) {
79             super.onCancelButtonClick(view);
80         }
81     }
82 
83     @Override
onSkipButtonClick(View view)84     protected void onSkipButtonClick(View view) {
85         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
86                 "skip")) {
87             super.onSkipButtonClick(view);
88         }
89     }
90 
91     @Override
onEnrollmentSkipped(@ullable Intent data)92     protected void onEnrollmentSkipped(@Nullable Intent data) {
93         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
94                 "skipped")) {
95             super.onEnrollmentSkipped(data);
96         }
97     }
98 
99     @Override
onFinishedEnrolling(@ullable Intent data)100     protected void onFinishedEnrolling(@Nullable Intent data) {
101         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
102                 "finished")) {
103             super.onFinishedEnrolling(data);
104         }
105     }
106 
107     @Override
shouldFinishWhenBackgrounded()108     protected boolean shouldFinishWhenBackgrounded() {
109         return super.shouldFinishWhenBackgrounded() && !BiometricUtils.isPostureGuidanceShowing(
110                 mDevicePostureState, mLaunchedPostureGuidance);
111     }
112 
113     @Override
onCreate(Bundle savedInstanceState)114     protected void onCreate(Bundle savedInstanceState) {
115         mFaceManager = getFaceManager();
116 
117         if (savedInstanceState == null
118                 && !WizardManagerHelper.isAnySetupWizard(getIntent())
119                 && !getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false)
120                 && maxFacesEnrolled()) {
121             // from tips && maxEnrolled
122             Log.d(TAG, "launch face settings");
123             launchFaceSettingsActivity();
124             finish();
125         }
126 
127         super.onCreate(savedInstanceState);
128 
129         // Wait super::onCreated() then return because SuperNotCalledExceptio will be thrown
130         // if we don't wait for it.
131         if (isFinishing()) {
132             return;
133         }
134 
135         // Apply extracted theme color to icons.
136         final ImageView iconGlasses = findViewById(R.id.icon_glasses);
137         final ImageView iconLooking = findViewById(R.id.icon_looking);
138         iconGlasses.getBackground().setColorFilter(getIconColorFilter());
139         iconLooking.getBackground().setColorFilter(getIconColorFilter());
140 
141         // Set text for views with multiple variations.
142         final TextView infoMessageGlasses = findViewById(R.id.info_message_glasses);
143         final TextView infoMessageLooking = findViewById(R.id.info_message_looking);
144         final TextView howMessage = findViewById(R.id.how_message);
145         final TextView inControlTitle = findViewById(R.id.title_in_control);
146         final TextView inControlMessage = findViewById(R.id.message_in_control);
147         final TextView lessSecure = findViewById(R.id.info_message_less_secure);
148         infoMessageGlasses.setText(getInfoMessageGlasses());
149         infoMessageLooking.setText(getInfoMessageLooking());
150         inControlTitle.setText(getInControlTitle());
151         howMessage.setText(getHowMessage());
152         inControlMessage.setText(Html.fromHtml(getString(getInControlMessage()),
153                 Html.FROM_HTML_MODE_LEGACY));
154         inControlMessage.setMovementMethod(LinkMovementMethod.getInstance());
155         lessSecure.setText(getLessSecureMessage());
156 
157         // Set up and show the "less secure" info section if necessary.
158         if (getResources().getBoolean(R.bool.config_face_intro_show_less_secure)) {
159             final LinearLayout infoRowLessSecure = findViewById(R.id.info_row_less_secure);
160             final ImageView iconLessSecure = findViewById(R.id.icon_less_secure);
161             infoRowLessSecure.setVisibility(View.VISIBLE);
162             iconLessSecure.getBackground().setColorFilter(getIconColorFilter());
163         }
164 
165         // Set up and show the "require eyes" info section if necessary.
166         if (getResources().getBoolean(R.bool.config_face_intro_show_require_eyes)) {
167             final LinearLayout infoRowRequireEyes = findViewById(R.id.info_row_require_eyes);
168             final ImageView iconRequireEyes = findViewById(R.id.icon_require_eyes);
169             final TextView infoMessageRequireEyes = findViewById(R.id.info_message_require_eyes);
170             infoRowRequireEyes.setVisibility(View.VISIBLE);
171             iconRequireEyes.getBackground().setColorFilter(getIconColorFilter());
172             infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
173         }
174 
175         // This path is an entry point for SetNewPasswordController, e.g.
176         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
177         if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
178             if (generateChallengeOnCreate()) {
179                 mFooterBarMixin.getPrimaryButton().setEnabled(false);
180                 // We either block on generateChallenge, or need to gray out the "next" button until
181                 // the challenge is ready. Let's just do this for now.
182                 mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
183                     if (isFinishing()) {
184                         // Do nothing if activity is finishing
185                         Log.w(TAG, "activity finished before challenge callback launched.");
186                         return;
187                     }
188 
189                     try {
190                         mToken = requestGatekeeperHat(challenge);
191                         mSensorId = sensorId;
192                         mChallenge = challenge;
193                         mFooterBarMixin.getPrimaryButton().setEnabled(true);
194                     } catch (GatekeeperCredentialNotMatchException e) {
195                         // Let BiometricEnrollBase#onCreate() to trigger confirmLock()
196                         getIntent().removeExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE);
197                         recreate();
198                     }
199                 });
200             }
201         }
202 
203         mSensorPrivacyManager = getApplicationContext()
204                 .getSystemService(SensorPrivacyManager.class);
205         final SensorPrivacyManagerHelper helper = SensorPrivacyManagerHelper
206                 .getInstance(getApplicationContext());
207         final boolean cameraPrivacyEnabled = helper
208                 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
209         Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled);
210     }
211 
launchFaceSettingsActivity()212     private void launchFaceSettingsActivity() {
213         final Intent intent = new Intent(this, Settings.FaceSettingsInternalActivity.class);
214         final byte[] token = getIntent().getByteArrayExtra(
215                 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
216         if (token != null) {
217             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
218         }
219         final int userId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
220         if (userId != UserHandle.USER_NULL) {
221             intent.putExtra(Intent.EXTRA_USER_ID, userId);
222         }
223         BiometricUtils.copyMultiBiometricExtras(getIntent(), intent);
224         intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true);
225         intent.putExtra(EXTRA_KEY_CHALLENGE, getIntent().getLongExtra(EXTRA_KEY_CHALLENGE, -1L));
226         intent.putExtra(EXTRA_KEY_SENSOR_ID, getIntent().getIntExtra(EXTRA_KEY_SENSOR_ID, -1));
227         startActivity(intent);
228     }
229 
230     @VisibleForTesting
231     @Nullable
getFaceManager()232     protected FaceManager getFaceManager() {
233         return Utils.getFaceManagerOrNull(this);
234     }
235 
236     @VisibleForTesting
237     @Nullable
getPostureGuidanceIntent()238     protected Intent getPostureGuidanceIntent() {
239         return mPostureGuidanceIntent;
240     }
241 
242     @VisibleForTesting
243     @Nullable
getPostureCallback()244     protected FoldProvider.FoldCallback getPostureCallback() {
245         return mFoldCallback;
246     }
247 
248     @VisibleForTesting
249     @BiometricUtils.DevicePostureInt
getDevicePostureState()250     protected int getDevicePostureState() {
251         return mDevicePostureState;
252     }
253 
254     @VisibleForTesting
255     @Nullable
requestGatekeeperHat(long challenge)256     protected byte[] requestGatekeeperHat(long challenge) {
257         return BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
258     }
259 
260     @Override
onConfigurationChanged(@onNull Configuration newConfig)261     public void onConfigurationChanged(@NonNull Configuration newConfig) {
262         super.onConfigurationChanged(newConfig);
263         if (mScreenSizeFoldProvider != null && getPostureCallback() != null) {
264             mScreenSizeFoldProvider.onConfigurationChange(newConfig);
265         }
266     }
267 
268     @Override
onStart()269     protected void onStart() {
270         super.onStart();
271         listenFoldEventForPostureGuidance();
272     }
273 
listenFoldEventForPostureGuidance()274     private void listenFoldEventForPostureGuidance() {
275         if (maxFacesEnrolled()) {
276             Log.d(TAG, "Device has enrolled face, do not show posture guidance");
277             return;
278         }
279 
280         if (getPostureGuidanceIntent() == null) {
281             Log.d(TAG, "Device do not support posture guidance");
282             return;
283         }
284 
285         BiometricUtils.setDevicePosturesAllowEnroll(
286                 getResources().getInteger(R.integer.config_face_enroll_supported_posture));
287 
288         if (getPostureCallback() == null) {
289             mFoldCallback = isFolded -> {
290                 mDevicePostureState = isFolded ? BiometricUtils.DEVICE_POSTURE_CLOSED
291                         : BiometricUtils.DEVICE_POSTURE_OPENED;
292                 if (BiometricUtils.shouldShowPostureGuidance(mDevicePostureState,
293                         mLaunchedPostureGuidance) && !mNextLaunched) {
294                     launchPostureGuidance();
295                 }
296             };
297         }
298 
299         if (mScreenSizeFoldProvider == null) {
300             mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext());
301             mScreenSizeFoldProvider.registerCallback(mFoldCallback, getMainExecutor());
302         }
303     }
304 
305     @Override
onActivityResult(int requestCode, int resultCode, Intent data)306     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
307         if (requestCode == REQUEST_POSTURE_GUIDANCE) {
308             mLaunchedPostureGuidance = false;
309             if (resultCode == RESULT_CANCELED || resultCode == RESULT_SKIP) {
310                 onSkipButtonClick(getCurrentFocus());
311             }
312             return;
313         }
314 
315         // If user has skipped or finished enrolling, don't restart enrollment.
316         final boolean isEnrollRequest = requestCode == BIOMETRIC_FIND_SENSOR_REQUEST
317                 || requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST;
318         final boolean isResultSkipOrFinished = resultCode == RESULT_SKIP
319                 || resultCode == SetupSkipDialog.RESULT_SKIP || resultCode == RESULT_FINISHED;
320         boolean hasEnrolledFace = false;
321         if (data != null) {
322             hasEnrolledFace = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FACE, false);
323         }
324 
325         if (resultCode == RESULT_CANCELED) {
326             if (hasEnrolledFace || !BiometricUtils.isPostureAllowEnrollment(mDevicePostureState)) {
327                 setResult(resultCode, data);
328                 finish();
329                 return;
330             }
331         }
332 
333         if (isEnrollRequest && isResultSkipOrFinished || hasEnrolledFace) {
334             data = setSkipPendingEnroll(data);
335         }
336         super.onActivityResult(requestCode, resultCode, data);
337     }
338 
generateChallengeOnCreate()339     protected boolean generateChallengeOnCreate() {
340         return true;
341     }
342 
343     @StringRes
getInfoMessageGlasses()344     protected int getInfoMessageGlasses() {
345         return R.string.security_settings_face_enroll_introduction_info_glasses;
346     }
347 
348     @StringRes
getInfoMessageLooking()349     protected int getInfoMessageLooking() {
350         return R.string.security_settings_face_enroll_introduction_info_looking;
351     }
352 
353     @StringRes
getInfoMessageRequireEyes()354     protected int getInfoMessageRequireEyes() {
355         return R.string.security_settings_face_enroll_introduction_info_gaze;
356     }
357 
358     @StringRes
getHowMessage()359     protected int getHowMessage() {
360         return R.string.security_settings_face_enroll_introduction_how_message;
361     }
362 
363     @StringRes
getInControlTitle()364     protected int getInControlTitle() {
365         return R.string.security_settings_face_enroll_introduction_control_title;
366     }
367 
368     @StringRes
getInControlMessage()369     protected int getInControlMessage() {
370         return R.string.security_settings_face_enroll_introduction_control_message;
371     }
372 
373     @StringRes
getLessSecureMessage()374     protected int getLessSecureMessage() {
375         return R.string.security_settings_face_enroll_introduction_info_less_secure;
376     }
377 
378     @Override
isDisabledByAdmin()379     protected boolean isDisabledByAdmin() {
380         return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
381                 this, DevicePolicyManager.KEYGUARD_DISABLE_FACE, mUserId) != null;
382     }
383 
384     @Override
getLayoutResource()385     protected int getLayoutResource() {
386         return R.layout.face_enroll_introduction;
387     }
388 
389     @Override
getHeaderResDisabledByAdmin()390     protected int getHeaderResDisabledByAdmin() {
391         return R.string.security_settings_face_enroll_introduction_title_unlock_disabled;
392     }
393 
394     @Override
getHeaderResDefault()395     protected int getHeaderResDefault() {
396         return R.string.security_settings_face_enroll_introduction_title;
397     }
398 
399     @Override
getDescriptionDisabledByAdmin()400     protected String getDescriptionDisabledByAdmin() {
401         DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class);
402         return devicePolicyManager.getResources().getString(
403                 FACE_UNLOCK_DISABLED,
404                 () -> getString(R.string.security_settings_face_enroll_introduction_message_unlock_disabled));
405     }
406 
407     @Override
getCancelButton()408     protected FooterButton getCancelButton() {
409         if (mFooterBarMixin != null) {
410             return mFooterBarMixin.getSecondaryButton();
411         }
412         return null;
413     }
414 
415     @Override
getNextButton()416     protected FooterButton getNextButton() {
417         if (mFooterBarMixin != null) {
418             return mFooterBarMixin.getPrimaryButton();
419         }
420         return null;
421     }
422 
423     @Override
getErrorTextView()424     protected TextView getErrorTextView() {
425         return findViewById(R.id.error_text);
426     }
427 
maxFacesEnrolled()428     private boolean maxFacesEnrolled() {
429         if (mFaceManager != null) {
430             // This will need to be updated for devices with multiple face sensors.
431             final int numEnrolledFaces = mFaceManager.getEnrolledFaces(mUserId).size();
432             final int maxFacesEnrollable = getApplicationContext().getResources()
433                     .getInteger(R.integer.suw_max_faces_enrollable);
434             return numEnrolledFaces >= maxFacesEnrollable;
435         } else {
436             return false;
437         }
438     }
439 
440     //TODO: Refactor this to something that conveys it is used for getting a string ID.
441     @Override
checkMaxEnrolled()442     protected int checkMaxEnrolled() {
443         if (mFaceManager != null) {
444             if (maxFacesEnrolled()) {
445                 return R.string.face_intro_error_max;
446             }
447         } else {
448             return R.string.face_intro_error_unknown;
449         }
450         return 0;
451     }
452 
453     @Override
getChallenge(GenerateChallengeCallback callback)454     protected void getChallenge(GenerateChallengeCallback callback) {
455         mFaceManager = Utils.getFaceManagerOrNull(this);
456         if (mFaceManager == null) {
457             callback.onChallengeGenerated(0, 0, 0L);
458             return;
459         }
460         mFaceManager.generateChallenge(mUserId, callback::onChallengeGenerated);
461     }
462 
463     @Override
getExtraKeyForBiometric()464     protected String getExtraKeyForBiometric() {
465         return ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE;
466     }
467 
468     @Override
getEnrollingIntent()469     protected Intent getEnrollingIntent() {
470         Intent intent = new Intent(this, FaceEnrollEducation.class);
471         WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent);
472         return intent;
473     }
474 
475     @Override
getConfirmLockTitleResId()476     protected int getConfirmLockTitleResId() {
477         return R.string.security_settings_face_preference_title;
478     }
479 
480     @Override
getMetricsCategory()481     public int getMetricsCategory() {
482         return SettingsEnums.FACE_ENROLL_INTRO;
483     }
484 
485     @Override
onClick(LinkSpan span)486     public void onClick(LinkSpan span) {
487         // TODO(b/110906762)
488     }
489 
490     @Override
getModality()491     public @BiometricAuthenticator.Modality int getModality() {
492         return BiometricAuthenticator.TYPE_FACE;
493     }
494 
495     @Override
onNextButtonClick(View view)496     protected void onNextButtonClick(View view) {
497         final boolean parentelConsentRequired =
498                 getIntent()
499                 .getBooleanExtra(BiometricEnrollActivity.EXTRA_REQUIRE_PARENTAL_CONSENT, false);
500         final boolean cameraPrivacyEnabled = SensorPrivacyManagerHelper
501                 .getInstance(getApplicationContext())
502                 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
503         final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
504         final boolean isSettingUp = isSetupWizard || (parentelConsentRequired
505                 && !WizardManagerHelper.isUserSetupComplete(this));
506         if (cameraPrivacyEnabled && !isSettingUp) {
507             if (mSensorPrivacyManager == null) {
508                 mSensorPrivacyManager = getApplicationContext()
509                         .getSystemService(SensorPrivacyManager.class);
510             }
511             mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.CAMERA);
512         } else {
513             super.onNextButtonClick(view);
514         }
515     }
516 
517     @Override
518     @NonNull
getPrimaryFooterButton()519     protected FooterButton getPrimaryFooterButton() {
520         if (mPrimaryFooterButton == null) {
521             mPrimaryFooterButton = new FooterButton.Builder(this)
522                     .setText(R.string.security_settings_face_enroll_introduction_agree)
523                     .setButtonType(FooterButton.ButtonType.OPT_IN)
524                     .setListener(this::onNextButtonClick)
525                     .setTheme(R.style.SudGlifButton_Primary)
526                     .build();
527         }
528         return mPrimaryFooterButton;
529     }
530 
531     @Override
532     @NonNull
getSecondaryFooterButton()533     protected FooterButton getSecondaryFooterButton() {
534         if (mSecondaryFooterButton == null) {
535             mSecondaryFooterButton = new FooterButton.Builder(this)
536                     .setText(R.string.security_settings_face_enroll_introduction_no_thanks)
537                     .setListener(this::onSkipButtonClick)
538                     .setButtonType(FooterButton.ButtonType.NEXT)
539                     .setTheme(R.style.SudGlifButton_Primary)
540                     .build();
541         }
542         return mSecondaryFooterButton;
543     }
544 
545     @Override
546     @StringRes
getAgreeButtonTextRes()547     protected int getAgreeButtonTextRes() {
548         return R.string.security_settings_fingerprint_enroll_introduction_agree;
549     }
550 
551     @Override
552     @StringRes
getMoreButtonTextRes()553     protected int getMoreButtonTextRes() {
554         return R.string.security_settings_face_enroll_introduction_more;
555     }
556 
557     @NonNull
setSkipPendingEnroll(@ullable Intent data)558     protected static Intent setSkipPendingEnroll(@Nullable Intent data) {
559         if (data == null) {
560             data = new Intent();
561         }
562         data.putExtra(MultiBiometricEnrollHelper.EXTRA_SKIP_PENDING_ENROLL, true);
563         return data;
564     }
565 }
566