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