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