• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.settings.biometrics.BiometricUtils.isPostureAllowEnrollment;
20 import static com.android.settings.biometrics.BiometricUtils.isPostureGuidanceShowing;
21 
22 import android.app.settings.SettingsEnums;
23 import android.content.ComponentName;
24 import android.content.Intent;
25 import android.content.res.Configuration;
26 import android.hardware.face.FaceManager;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.text.TextUtils;
30 import android.util.Log;
31 import android.view.View;
32 import android.view.accessibility.AccessibilityManager;
33 import android.widget.Button;
34 import android.widget.CompoundButton;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.settings.R;
41 import com.android.settings.Utils;
42 import com.android.settings.biometrics.BiometricEnrollBase;
43 import com.android.settings.biometrics.BiometricUtils;
44 import com.android.settings.password.ChooseLockSettingsHelper;
45 import com.android.settings.password.SetupSkipDialog;
46 import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
47 import com.android.systemui.unfold.updates.FoldProvider;
48 
49 import com.airbnb.lottie.LottieAnimationView;
50 import com.google.android.setupcompat.template.FooterBarMixin;
51 import com.google.android.setupcompat.template.FooterButton;
52 import com.google.android.setupcompat.util.WizardManagerHelper;
53 import com.google.android.setupdesign.view.IllustrationVideoView;
54 
55 /**
56  * Provides animated education for users to know how to enroll a face with appropriate posture.
57  */
58 public class FaceEnrollEducation extends BiometricEnrollBase {
59     private static final String TAG = "FaceEducation";
60 
61     private FaceManager mFaceManager;
62     private FaceEnrollAccessibilityToggle mSwitchDiversity;
63     private boolean mIsUsingLottie;
64     private IllustrationVideoView mIllustrationDefault;
65     private LottieAnimationView mIllustrationLottie;
66     private View mIllustrationAccessibility;
67     private Intent mResultIntent;
68     private boolean mAccessibilityEnabled;
69 
70     private final CompoundButton.OnCheckedChangeListener mSwitchDiversityListener =
71             new CompoundButton.OnCheckedChangeListener() {
72                 @Override
73                 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
74                     final int descriptionRes = isChecked
75                             ? R.string.security_settings_face_enroll_education_message_accessibility
76                             : R.string.security_settings_face_enroll_education_message;
77                     setDescriptionText(descriptionRes);
78 
79                     if (isChecked) {
80                         hideDefaultIllustration();
81                         mIllustrationAccessibility.setVisibility(View.VISIBLE);
82                     } else {
83                         showDefaultIllustration();
84                         mIllustrationAccessibility.setVisibility(View.INVISIBLE);
85                     }
86                 }
87             };
88 
89     @Override
onCreate(Bundle savedInstanceState)90     protected void onCreate(Bundle savedInstanceState) {
91         super.onCreate(savedInstanceState);
92         setContentView(R.layout.face_enroll_education);
93 
94         setTitle(R.string.security_settings_face_enroll_education_title);
95         setDescriptionText(R.string.security_settings_face_enroll_education_message);
96 
97         mFaceManager = Utils.getFaceManagerOrNull(this);
98 
99         mIllustrationDefault = findViewById(R.id.illustration_default);
100         mIllustrationLottie = findViewById(R.id.illustration_lottie);
101         mIllustrationAccessibility = findViewById(R.id.illustration_accessibility);
102 
103         mIsUsingLottie = getResources().getBoolean(R.bool.config_face_education_use_lottie);
104         if (mIsUsingLottie) {
105             mIllustrationDefault.stop();
106             mIllustrationDefault.setVisibility(View.INVISIBLE);
107             mIllustrationLottie.setAnimation(R.raw.face_education_lottie);
108             mIllustrationLottie.setVisibility(View.VISIBLE);
109             mIllustrationLottie.playAnimation();
110         }
111 
112         mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
113 
114         if (WizardManagerHelper.isAnySetupWizard(getIntent())) {
115             mFooterBarMixin.setSecondaryButton(
116                     new FooterButton.Builder(this)
117                             .setText(R.string.skip_label)
118                             .setListener(this::onSkipButtonClick)
119                             .setButtonType(FooterButton.ButtonType.SKIP)
120                             .setTheme(R.style.SudGlifButton_Secondary)
121                             .build()
122             );
123         } else {
124             mFooterBarMixin.setSecondaryButton(
125                     new FooterButton.Builder(this)
126                             .setText(R.string.security_settings_face_enroll_introduction_cancel)
127                             .setListener(this::onSkipButtonClick)
128                             .setButtonType(FooterButton.ButtonType.CANCEL)
129                             .setTheme(R.style.SudGlifButton_Secondary)
130                             .build()
131             );
132         }
133 
134         final FooterButton footerButton = new FooterButton.Builder(this)
135                 .setText(R.string.security_settings_face_enroll_education_start)
136                 .setListener(this::onNextButtonClick)
137                 .setButtonType(FooterButton.ButtonType.NEXT)
138                 .setTheme(R.style.SudGlifButton_Primary)
139                 .build();
140 
141         final AccessibilityManager accessibilityManager = getApplicationContext().getSystemService(
142                 AccessibilityManager.class);
143         if (accessibilityManager != null) {
144             // Add additional check for touch exploration. This prevents other accessibility
145             // features such as Live Transcribe from defaulting to the accessibility setup.
146             mAccessibilityEnabled = accessibilityManager.isEnabled()
147                     && accessibilityManager.isTouchExplorationEnabled();
148         }
149         mFooterBarMixin.setPrimaryButton(footerButton);
150 
151         final Button accessibilityButton = findViewById(R.id.accessibility_button);
152         accessibilityButton.setOnClickListener(view -> {
153             mSwitchDiversity.setChecked(true);
154             accessibilityButton.setVisibility(View.GONE);
155             mSwitchDiversity.setVisibility(View.VISIBLE);
156         });
157 
158         mSwitchDiversity = findViewById(R.id.toggle_diversity);
159         mSwitchDiversity.setListener(mSwitchDiversityListener);
160         mSwitchDiversity.setOnClickListener(v -> {
161             mSwitchDiversity.getSwitch().toggle();
162         });
163 
164         if (mAccessibilityEnabled) {
165             accessibilityButton.callOnClick();
166         }
167     }
168 
169     @Override
onStart()170     protected void onStart() {
171         super.onStart();
172         if (getPostureGuidanceIntent() == null) {
173             Log.d(TAG, "Device do not support posture guidance");
174             return;
175         }
176 
177         BiometricUtils.setDevicePosturesAllowEnroll(
178                 getResources().getInteger(R.integer.config_face_enroll_supported_posture));
179 
180         if (getPostureCallback() == null) {
181             mFoldCallback = isFolded -> {
182                 mDevicePostureState = isFolded ? BiometricUtils.DEVICE_POSTURE_CLOSED
183                         : BiometricUtils.DEVICE_POSTURE_OPENED;
184                 if (BiometricUtils.shouldShowPostureGuidance(mDevicePostureState,
185                         mLaunchedPostureGuidance) && !mNextLaunched) {
186                     launchPostureGuidance();
187                 }
188             };
189         }
190 
191         if (mScreenSizeFoldProvider == null) {
192             mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext());
193             mScreenSizeFoldProvider.registerCallback(mFoldCallback, getMainExecutor());
194         }
195     }
196 
197     @Override
onResume()198     protected void onResume() {
199         super.onResume();
200         mSwitchDiversityListener.onCheckedChanged(mSwitchDiversity.getSwitch(),
201                 mSwitchDiversity.isChecked());
202 
203         // If the user goes back after enrollment, we should send them back to the intro page
204         // if they've met the max limit.
205         final int max = getResources().getInteger(
206                 com.android.internal.R.integer.config_faceMaxTemplatesPerUser);
207         final int numEnrolledFaces = mFaceManager.getEnrolledFaces(mUserId).size();
208         if (numEnrolledFaces >= max) {
209             finish();
210         }
211     }
212 
213     @Override
shouldFinishWhenBackgrounded()214     protected boolean shouldFinishWhenBackgrounded() {
215         return super.shouldFinishWhenBackgrounded() && !mNextLaunched
216                 && !isPostureGuidanceShowing(mDevicePostureState, mLaunchedPostureGuidance);
217     }
218 
219     @Override
onNextButtonClick(View view)220     protected void onNextButtonClick(View view) {
221         final Intent intent = new Intent();
222         if (mToken != null) {
223             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
224         }
225         if (mUserId != UserHandle.USER_NULL) {
226             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
227         }
228         intent.putExtra(EXTRA_KEY_CHALLENGE, mChallenge);
229         intent.putExtra(EXTRA_KEY_SENSOR_ID, mSensorId);
230         intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary);
231         BiometricUtils.copyMultiBiometricExtras(getIntent(), intent);
232         final String flattenedString = getString(R.string.config_face_enroll);
233         if (!TextUtils.isEmpty(flattenedString)) {
234             ComponentName componentName = ComponentName.unflattenFromString(flattenedString);
235             intent.setComponent(componentName);
236         } else {
237             intent.setClass(this, FaceEnrollEnrolling.class);
238         }
239         WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent);
240         if (mResultIntent != null) {
241             intent.putExtras(mResultIntent);
242         }
243 
244         intent.putExtra(EXTRA_KEY_REQUIRE_DIVERSITY, !mSwitchDiversity.isChecked());
245 
246         if (!mSwitchDiversity.isChecked() && mAccessibilityEnabled) {
247             FaceEnrollAccessibilityDialog dialog = FaceEnrollAccessibilityDialog.newInstance();
248             dialog.setPositiveButtonListener((dialog1, which) -> {
249                 startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
250                 mNextLaunched = true;
251             });
252             dialog.show(getSupportFragmentManager(), FaceEnrollAccessibilityDialog.class.getName());
253         } else {
254             startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
255             mNextLaunched = true;
256         }
257 
258     }
259 
onSkipButtonClick(View view)260     protected void onSkipButtonClick(View view) {
261         if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST,
262                 "edu_skip")) {
263             setResult(RESULT_SKIP);
264             finish();
265         }
266     }
267 
268     @Override
onConfigurationChanged(@onNull Configuration newConfig)269     public void onConfigurationChanged(@NonNull Configuration newConfig) {
270         super.onConfigurationChanged(newConfig);
271         if (mScreenSizeFoldProvider != null && getPostureCallback() != null) {
272             mScreenSizeFoldProvider.onConfigurationChange(newConfig);
273         }
274     }
275 
276     @Override
onActivityResult(int requestCode, int resultCode, Intent data)277     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
278         if (requestCode == REQUEST_POSTURE_GUIDANCE) {
279             mLaunchedPostureGuidance = false;
280             if (resultCode == RESULT_CANCELED || resultCode == RESULT_SKIP) {
281                 onSkipButtonClick(getCurrentFocus());
282             }
283             return;
284         }
285         mResultIntent = data;
286         boolean hasEnrolledFace = false;
287         if (data != null) {
288             hasEnrolledFace = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FACE, false);
289         }
290         if (resultCode == RESULT_TIMEOUT || !isPostureAllowEnrollment(mDevicePostureState)) {
291             setResult(resultCode, data);
292             finish();
293         } else if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST
294                 || requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST) {
295             // If the user finished or skipped enrollment, finish this activity
296             if (resultCode == RESULT_SKIP || resultCode == RESULT_FINISHED
297                     || resultCode == SetupSkipDialog.RESULT_SKIP || hasEnrolledFace) {
298                 setResult(resultCode, data);
299                 finish();
300             }
301         }
302         mNextLaunched = false;
303         super.onActivityResult(requestCode, resultCode, data);
304     }
305 
306     @VisibleForTesting
307     @Nullable
getPostureGuidanceIntent()308     protected Intent getPostureGuidanceIntent() {
309         return mPostureGuidanceIntent;
310     }
311 
312     @VisibleForTesting
313     @Nullable
getPostureCallback()314     protected FoldProvider.FoldCallback getPostureCallback() {
315         return mFoldCallback;
316     }
317 
318     @VisibleForTesting
319     @BiometricUtils.DevicePostureInt
getDevicePostureState()320     protected int getDevicePostureState() {
321         return mDevicePostureState;
322     }
323 
324     @Override
getMetricsCategory()325     public int getMetricsCategory() {
326         return SettingsEnums.FACE_ENROLL_INTRO;
327     }
328 
hideDefaultIllustration()329     private void hideDefaultIllustration() {
330         if (mIsUsingLottie) {
331             mIllustrationLottie.cancelAnimation();
332             mIllustrationLottie.setVisibility(View.INVISIBLE);
333         } else {
334             mIllustrationDefault.stop();
335             mIllustrationDefault.setVisibility(View.INVISIBLE);
336         }
337     }
338 
showDefaultIllustration()339     private void showDefaultIllustration() {
340         if (mIsUsingLottie) {
341             mIllustrationLottie.setAnimation(R.raw.face_education_lottie);
342             mIllustrationLottie.setVisibility(View.VISIBLE);
343             mIllustrationLottie.playAnimation();
344             mIllustrationLottie.setProgress(0f);
345         } else {
346             mIllustrationDefault.setVisibility(View.VISIBLE);
347             mIllustrationDefault.start();
348         }
349     }
350 }
351