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 android.app.admin.DevicePolicyManager;
20 import android.app.settings.SettingsEnums;
21 import android.content.Intent;
22 import android.hardware.SensorPrivacyManager;
23 import android.hardware.biometrics.BiometricAuthenticator;
24 import android.hardware.face.FaceManager;
25 import android.hardware.face.FaceSensorPropertiesInternal;
26 import android.os.Bundle;
27 import android.util.Log;
28 import android.view.View;
29 import android.widget.ImageView;
30 import android.widget.LinearLayout;
31 import android.widget.TextView;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.StringRes;
36 
37 import com.android.settings.R;
38 import com.android.settings.Utils;
39 import com.android.settings.biometrics.BiometricEnrollActivity;
40 import com.android.settings.biometrics.BiometricEnrollIntroduction;
41 import com.android.settings.biometrics.BiometricUtils;
42 import com.android.settings.overlay.FeatureFactory;
43 import com.android.settings.password.ChooseLockSettingsHelper;
44 import com.android.settings.utils.SensorPrivacyManagerHelper;
45 import com.android.settingslib.RestrictedLockUtilsInternal;
46 
47 import com.google.android.setupcompat.template.FooterButton;
48 import com.google.android.setupcompat.util.WizardManagerHelper;
49 import com.google.android.setupdesign.span.LinkSpan;
50 
51 import java.util.List;
52 
53 /**
54  * Provides introductory info about face unlock and prompts the user to agree before starting face
55  * enrollment.
56  */
57 public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
58     private static final String TAG = "FaceEnrollIntroduction";
59 
60     private FaceManager mFaceManager;
61     private FaceFeatureProvider mFaceFeatureProvider;
62     @Nullable private FooterButton mPrimaryFooterButton;
63     @Nullable private FooterButton mSecondaryFooterButton;
64     @Nullable private SensorPrivacyManager mSensorPrivacyManager;
65 
66     @Override
onCancelButtonClick(View view)67     protected void onCancelButtonClick(View view) {
68         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
69                 "cancel")) {
70             super.onCancelButtonClick(view);
71         }
72     }
73 
74     @Override
onSkipButtonClick(View view)75     protected void onSkipButtonClick(View view) {
76         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
77                 "skip")) {
78             super.onSkipButtonClick(view);
79         }
80     }
81 
82     @Override
onEnrollmentSkipped(@ullable Intent data)83     protected void onEnrollmentSkipped(@Nullable Intent data) {
84         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
85                 "skipped")) {
86             super.onEnrollmentSkipped(data);
87         }
88     }
89 
90     @Override
onFinishedEnrolling(@ullable Intent data)91     protected void onFinishedEnrolling(@Nullable Intent data) {
92         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
93                 "finished")) {
94             super.onFinishedEnrolling(data);
95         }
96     }
97 
98     @Override
onCreate(Bundle savedInstanceState)99     protected void onCreate(Bundle savedInstanceState) {
100         super.onCreate(savedInstanceState);
101 
102         // Apply extracted theme color to icons.
103         final ImageView iconGlasses = findViewById(R.id.icon_glasses);
104         final ImageView iconLooking = findViewById(R.id.icon_looking);
105         iconGlasses.getBackground().setColorFilter(getIconColorFilter());
106         iconLooking.getBackground().setColorFilter(getIconColorFilter());
107 
108         // Set text for views with multiple variations.
109         final TextView infoMessageGlasses = findViewById(R.id.info_message_glasses);
110         final TextView infoMessageLooking = findViewById(R.id.info_message_looking);
111         final TextView howMessage = findViewById(R.id.how_message);
112         final TextView inControlTitle = findViewById(R.id.title_in_control);
113         final TextView inControlMessage = findViewById(R.id.message_in_control);
114         infoMessageGlasses.setText(getInfoMessageGlasses());
115         infoMessageLooking.setText(getInfoMessageLooking());
116         inControlTitle.setText(getInControlTitle());
117         howMessage.setText(getHowMessage());
118         inControlMessage.setText(getInControlMessage());
119 
120         // Set up and show the "less secure" info section if necessary.
121         if (getResources().getBoolean(R.bool.config_face_intro_show_less_secure)) {
122             final LinearLayout infoRowLessSecure = findViewById(R.id.info_row_less_secure);
123             final ImageView iconLessSecure = findViewById(R.id.icon_less_secure);
124             infoRowLessSecure.setVisibility(View.VISIBLE);
125             iconLessSecure.getBackground().setColorFilter(getIconColorFilter());
126         }
127 
128         // Set up and show the "require eyes" info section if necessary.
129         if (getResources().getBoolean(R.bool.config_face_intro_show_require_eyes)) {
130             final LinearLayout infoRowRequireEyes = findViewById(R.id.info_row_require_eyes);
131             final ImageView iconRequireEyes = findViewById(R.id.icon_require_eyes);
132             final TextView infoMessageRequireEyes = findViewById(R.id.info_message_require_eyes);
133             infoRowRequireEyes.setVisibility(View.VISIBLE);
134             iconRequireEyes.getBackground().setColorFilter(getIconColorFilter());
135             infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
136         }
137 
138         mFaceManager = Utils.getFaceManagerOrNull(this);
139         mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext())
140                 .getFaceFeatureProvider();
141 
142         // This path is an entry point for SetNewPasswordController, e.g.
143         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
144         if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
145             if (generateChallengeOnCreate()) {
146                 mFooterBarMixin.getPrimaryButton().setEnabled(false);
147                 // We either block on generateChallenge, or need to gray out the "next" button until
148                 // the challenge is ready. Let's just do this for now.
149                 mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
150                     mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId,
151                             challenge);
152                     mSensorId = sensorId;
153                     mChallenge = challenge;
154                     mFooterBarMixin.getPrimaryButton().setEnabled(true);
155                 });
156             }
157         }
158 
159         mSensorPrivacyManager = getApplicationContext()
160                 .getSystemService(SensorPrivacyManager.class);
161         final SensorPrivacyManagerHelper helper = SensorPrivacyManagerHelper
162                 .getInstance(getApplicationContext());
163         final boolean cameraPrivacyEnabled = helper
164                 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
165         Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled);
166     }
167 
generateChallengeOnCreate()168     protected boolean generateChallengeOnCreate() {
169         return true;
170     }
171 
172     @StringRes
getInfoMessageGlasses()173     protected int getInfoMessageGlasses() {
174         return R.string.security_settings_face_enroll_introduction_info_glasses;
175     }
176 
177     @StringRes
getInfoMessageLooking()178     protected int getInfoMessageLooking() {
179         return R.string.security_settings_face_enroll_introduction_info_looking;
180     }
181 
182     @StringRes
getInfoMessageRequireEyes()183     protected int getInfoMessageRequireEyes() {
184         return R.string.security_settings_face_enroll_introduction_info_gaze;
185     }
186 
187     @StringRes
getHowMessage()188     protected int getHowMessage() {
189         return R.string.security_settings_face_enroll_introduction_how_message;
190     }
191 
192     @StringRes
getInControlTitle()193     protected int getInControlTitle() {
194         return R.string.security_settings_face_enroll_introduction_control_title;
195     }
196 
197     @StringRes
getInControlMessage()198     protected int getInControlMessage() {
199         return R.string.security_settings_face_enroll_introduction_control_message;
200     }
201 
202     @Override
isDisabledByAdmin()203     protected boolean isDisabledByAdmin() {
204         return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
205                 this, DevicePolicyManager.KEYGUARD_DISABLE_FACE, mUserId) != null;
206     }
207 
208     @Override
getLayoutResource()209     protected int getLayoutResource() {
210         return R.layout.face_enroll_introduction;
211     }
212 
213     @Override
getHeaderResDisabledByAdmin()214     protected int getHeaderResDisabledByAdmin() {
215         return R.string.security_settings_face_enroll_introduction_title_unlock_disabled;
216     }
217 
218     @Override
getHeaderResDefault()219     protected int getHeaderResDefault() {
220         return R.string.security_settings_face_enroll_introduction_title;
221     }
222 
223     @Override
getDescriptionResDisabledByAdmin()224     protected int getDescriptionResDisabledByAdmin() {
225         return R.string.security_settings_face_enroll_introduction_message_unlock_disabled;
226     }
227 
228     @Override
getCancelButton()229     protected FooterButton getCancelButton() {
230         if (mFooterBarMixin != null) {
231             return mFooterBarMixin.getSecondaryButton();
232         }
233         return null;
234     }
235 
236     @Override
getNextButton()237     protected FooterButton getNextButton() {
238         if (mFooterBarMixin != null) {
239             return mFooterBarMixin.getPrimaryButton();
240         }
241         return null;
242     }
243 
244     @Override
getErrorTextView()245     protected TextView getErrorTextView() {
246         return findViewById(R.id.error_text);
247     }
248 
maxFacesEnrolled()249     private boolean maxFacesEnrolled() {
250         final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
251         if (mFaceManager != null) {
252             final List<FaceSensorPropertiesInternal> props =
253                     mFaceManager.getSensorPropertiesInternal();
254             // This will need to be updated for devices with multiple face sensors.
255             final int max = props.get(0).maxEnrollmentsPerUser;
256             final int numEnrolledFaces = mFaceManager.getEnrolledFaces(mUserId).size();
257             final int maxFacesEnrollableIfSUW = getApplicationContext().getResources()
258                     .getInteger(R.integer.suw_max_faces_enrollable);
259             if (isSetupWizard) {
260                 return numEnrolledFaces >= maxFacesEnrollableIfSUW;
261             } else {
262                 return numEnrolledFaces >= max;
263             }
264         } else {
265             return false;
266         }
267     }
268 
269     //TODO: Refactor this to something that conveys it is used for getting a string ID.
270     @Override
checkMaxEnrolled()271     protected int checkMaxEnrolled() {
272         if (mFaceManager != null) {
273             if (maxFacesEnrolled()) {
274                 return R.string.face_intro_error_max;
275             }
276         } else {
277             return R.string.face_intro_error_unknown;
278         }
279         return 0;
280     }
281 
282     @Override
getChallenge(GenerateChallengeCallback callback)283     protected void getChallenge(GenerateChallengeCallback callback) {
284         mFaceManager = Utils.getFaceManagerOrNull(this);
285         if (mFaceManager == null) {
286             callback.onChallengeGenerated(0, 0, 0L);
287             return;
288         }
289         mFaceManager.generateChallenge(mUserId, callback::onChallengeGenerated);
290     }
291 
292     @Override
getExtraKeyForBiometric()293     protected String getExtraKeyForBiometric() {
294         return ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE;
295     }
296 
297     @Override
getEnrollingIntent()298     protected Intent getEnrollingIntent() {
299         Intent intent = new Intent(this, FaceEnrollEducation.class);
300         WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent);
301         return intent;
302     }
303 
304     @Override
getConfirmLockTitleResId()305     protected int getConfirmLockTitleResId() {
306         return R.string.security_settings_face_preference_title;
307     }
308 
309     @Override
getMetricsCategory()310     public int getMetricsCategory() {
311         return SettingsEnums.FACE_ENROLL_INTRO;
312     }
313 
314     @Override
onClick(LinkSpan span)315     public void onClick(LinkSpan span) {
316         // TODO(b/110906762)
317     }
318 
319     @Override
getModality()320     public @BiometricAuthenticator.Modality int getModality() {
321         return BiometricAuthenticator.TYPE_FACE;
322     }
323 
324     @Override
onNextButtonClick(View view)325     protected void onNextButtonClick(View view) {
326         final boolean parentelConsentRequired =
327                 getIntent()
328                 .getBooleanExtra(BiometricEnrollActivity.EXTRA_REQUIRE_PARENTAL_CONSENT, false);
329         final boolean cameraPrivacyEnabled = SensorPrivacyManagerHelper
330                 .getInstance(getApplicationContext())
331                 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId);
332         final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
333         final boolean isSettingUp = isSetupWizard || (parentelConsentRequired
334                 && !WizardManagerHelper.isUserSetupComplete(this));
335         if (cameraPrivacyEnabled && !isSettingUp) {
336             if (mSensorPrivacyManager == null) {
337                 mSensorPrivacyManager = getApplicationContext()
338                         .getSystemService(SensorPrivacyManager.class);
339             }
340             mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.CAMERA);
341         } else {
342             super.onNextButtonClick(view);
343         }
344     }
345 
346     @Override
347     @NonNull
getPrimaryFooterButton()348     protected FooterButton getPrimaryFooterButton() {
349         if (mPrimaryFooterButton == null) {
350             mPrimaryFooterButton = new FooterButton.Builder(this)
351                     .setText(R.string.security_settings_face_enroll_introduction_agree)
352                     .setButtonType(FooterButton.ButtonType.OPT_IN)
353                     .setListener(this::onNextButtonClick)
354                     .setTheme(R.style.SudGlifButton_Primary)
355                     .build();
356         }
357         return mPrimaryFooterButton;
358     }
359 
360     @Override
361     @NonNull
getSecondaryFooterButton()362     protected FooterButton getSecondaryFooterButton() {
363         if (mSecondaryFooterButton == null) {
364             mSecondaryFooterButton = new FooterButton.Builder(this)
365                     .setText(R.string.security_settings_face_enroll_introduction_no_thanks)
366                     .setListener(this::onSkipButtonClick)
367                     .setButtonType(FooterButton.ButtonType.NEXT)
368                     .setTheme(R.style.SudGlifButton_Primary)
369                     .build();
370         }
371         return mSecondaryFooterButton;
372     }
373 
374     @Override
375     @StringRes
getAgreeButtonTextRes()376     protected int getAgreeButtonTextRes() {
377         return R.string.security_settings_fingerprint_enroll_introduction_agree;
378     }
379 
380     @Override
381     @StringRes
getMoreButtonTextRes()382     protected int getMoreButtonTextRes() {
383         return R.string.security_settings_face_enroll_introduction_more;
384     }
385 }
386