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