diff --git a/res/raw/face_posture_guidance_lottie.json b/res/raw/face_posture_guidance_lottie.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/res/values/config.xml b/res/values/config.xml
index 3ba98f01b73..5cfa1883eab 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -283,6 +283,19 @@
+
+
+
+
+ 0
+
false
diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
index 03bff7bfa30..369f453fc1b 100644
--- a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
+++ b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
@@ -36,6 +36,7 @@ import androidx.window.embedding.SplitRule;
import com.android.settings.Settings;
import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
+import com.android.settings.biometrics.face.FaceEnrollIntroductionInternal;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
@@ -228,6 +229,8 @@ public class ActivityEmbeddingRulesController {
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
+ addActivityFilter(activityFilters, FaceEnrollIntroductionInternal.class);
+ addActivityFilter(activityFilters, Settings.FaceSettingsInternalActivity.class);
addActivityFilter(activityFilters, AvatarPickerActivity.class);
mSplitController.registerRule(new ActivityRule(activityFilters, true /* alwaysExpand */));
}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index eea1bad91a9..2f852f08b9f 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -38,7 +38,10 @@ import com.android.settings.SetupWizardUtils;
import com.android.settings.Utils;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.core.InstrumentedActivity;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
+import com.android.systemui.unfold.updates.FoldProvider;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
@@ -60,8 +63,10 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
public static final String EXTRA_KEY_SENSOR_ID = "sensor_id";
public static final String EXTRA_KEY_CHALLENGE = "challenge";
public static final String EXTRA_KEY_MODALITY = "sensor_modality";
+ public static final String EXTRA_KEY_NEXT_LAUNCHED = "next_launched";
public static final String EXTRA_FINISHED_ENROLL_FACE = "finished_enrolling_face";
public static final String EXTRA_FINISHED_ENROLL_FINGERPRINT = "finished_enrolling_fingerprint";
+ public static final String EXTRA_LAUNCHED_POSTURE_GUIDANCE = "launched_posture_guidance";
/**
* Used by the choose fingerprint wizard to indicate the wizard is
@@ -115,14 +120,25 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
* example, when starting fingerprint enroll after face enroll.
*/
public static final int ENROLL_NEXT_BIOMETRIC_REQUEST = 6;
+ public static final int REQUEST_POSTURE_GUIDANCE = 7;
protected boolean mLaunchedConfirmLock;
+ protected boolean mLaunchedPostureGuidance;
+ protected boolean mNextLaunched;
protected byte[] mToken;
protected int mUserId;
protected int mSensorId;
+ @BiometricUtils.DevicePostureInt
+ protected int mDevicePostureState;
protected long mChallenge;
protected boolean mFromSettingsSummary;
protected FooterBarMixin mFooterBarMixin;
+ @Nullable
+ protected ScreenSizeFoldProvider mScreenSizeFoldProvider;
+ @Nullable
+ protected Intent mPostureGuidanceIntent = null;
+ @Nullable
+ protected FoldProvider.FoldCallback mFoldCallback = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -139,16 +155,23 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
}
mFromSettingsSummary = getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false);
- if (savedInstanceState != null && mToken == null) {
- mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM);
- mToken = savedInstanceState.getByteArray(
- ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
- mFromSettingsSummary =
- savedInstanceState.getBoolean(EXTRA_FROM_SETTINGS_SUMMARY, false);
- mChallenge = savedInstanceState.getLong(EXTRA_KEY_CHALLENGE);
- mSensorId = savedInstanceState.getInt(EXTRA_KEY_SENSOR_ID);
+ if (savedInstanceState != null) {
+ if (mToken == null) {
+ mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM);
+ mToken = savedInstanceState.getByteArray(
+ ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
+ mFromSettingsSummary =
+ savedInstanceState.getBoolean(EXTRA_FROM_SETTINGS_SUMMARY, false);
+ mChallenge = savedInstanceState.getLong(EXTRA_KEY_CHALLENGE);
+ mSensorId = savedInstanceState.getInt(EXTRA_KEY_SENSOR_ID);
+ }
+ mLaunchedPostureGuidance = savedInstanceState.getBoolean(
+ EXTRA_LAUNCHED_POSTURE_GUIDANCE);
+ mNextLaunched = savedInstanceState.getBoolean(EXTRA_KEY_NEXT_LAUNCHED);
}
mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
+ mPostureGuidanceIntent = FeatureFactory.getFactory(getApplicationContext())
+ .getFaceFeatureProvider().getPostureGuidanceIntent(getApplicationContext());
}
@Override
@@ -159,6 +182,8 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
outState.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary);
outState.putLong(EXTRA_KEY_CHALLENGE, mChallenge);
outState.putInt(EXTRA_KEY_SENSOR_ID, mSensorId);
+ outState.putBoolean(EXTRA_LAUNCHED_POSTURE_GUIDANCE, mLaunchedPostureGuidance);
+ outState.putBoolean(EXTRA_KEY_NEXT_LAUNCHED, mNextLaunched);
}
@Override
@@ -184,6 +209,12 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
@Override
protected void onStop() {
super.onStop();
+ if (mScreenSizeFoldProvider != null && mFoldCallback != null) {
+ mScreenSizeFoldProvider.unregisterCallback(mFoldCallback);
+ }
+ mScreenSizeFoldProvider = null;
+ mFoldCallback = null;
+
if (!isChangingConfigurations() && shouldFinishWhenBackgrounded()
&& !BiometricUtils.isAnyMultiBiometricFlow(this)) {
setResult(RESULT_TIMEOUT);
@@ -191,6 +222,17 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
}
}
+ protected boolean launchPostureGuidance() {
+ if (mPostureGuidanceIntent == null || mLaunchedPostureGuidance) {
+ return false;
+ }
+ BiometricUtils.copyMultiBiometricExtras(getIntent(), mPostureGuidanceIntent);
+ startActivityForResult(mPostureGuidanceIntent, REQUEST_POSTURE_GUIDANCE);
+ mLaunchedPostureGuidance = true;
+ overridePendingTransition(0 /* no enter anim */, 0 /* no exit anim */);
+ return mLaunchedPostureGuidance;
+ }
+
protected boolean shouldFinishWhenBackgrounded() {
return !WizardManagerHelper.isAnySetupWizard(getIntent());
}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
index acfe5a10255..730e0496774 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
@@ -155,6 +155,8 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
if (savedInstanceState != null) {
mConfirmingCredentials = savedInstanceState.getBoolean(KEY_CONFIRMING_CREDENTIALS);
mHasScrolledToBottom = savedInstanceState.getBoolean(KEY_SCROLLED_TO_BOTTOM);
+ mLaunchedPostureGuidance = savedInstanceState.getBoolean(
+ EXTRA_LAUNCHED_POSTURE_GUIDANCE);
}
Intent intent = getIntent();
@@ -273,6 +275,7 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
finish();
}
}
+ mNextLaunched = true;
}
private void launchChooseLock() {
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index d8b6425260a..733308325f6 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -16,6 +16,7 @@
package com.android.settings.biometrics;
+import android.annotation.IntDef;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
@@ -47,12 +48,37 @@ import com.android.settings.password.SetupChooseLockGeneric;
import com.google.android.setupcompat.util.WizardManagerHelper;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Common biometric utilities.
*/
public class BiometricUtils {
private static final String TAG = "BiometricUtils";
+ // Note: Theis IntDef must align SystemUI DevicePostureInt
+ @IntDef(prefix = {"DEVICE_POSTURE_"}, value = {
+ DEVICE_POSTURE_UNKNOWN,
+ DEVICE_POSTURE_CLOSED,
+ DEVICE_POSTURE_HALF_OPENED,
+ DEVICE_POSTURE_OPENED,
+ DEVICE_POSTURE_FLIPPED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DevicePostureInt {}
+
+ // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we
+ // use the Device State -> Jetpack Posture map in DevicePostureControllerImpl to translate
+ // between the two.
+ public static final int DEVICE_POSTURE_UNKNOWN = 0;
+ public static final int DEVICE_POSTURE_CLOSED = 1;
+ public static final int DEVICE_POSTURE_HALF_OPENED = 2;
+ public static final int DEVICE_POSTURE_OPENED = 3;
+ public static final int DEVICE_POSTURE_FLIPPED = 4;
+
+ public static int sAllowEnrollPosture = DEVICE_POSTURE_UNKNOWN;
+
/**
* Request was sent for starting another enrollment of a previously
* enrolled biometric of the same type.
@@ -283,6 +309,51 @@ public class BiometricUtils {
|| isMultiBiometricFingerprintEnrollmentFlow(activity);
}
+ /**
+ * Used to check if the activity is showing a posture guidance to user.
+ *
+ * @param devicePosture the device posture state
+ * @param isLaunchedPostureGuidance True launching a posture guidance to user
+ * @return True if the activity is showing posture guidance to user
+ */
+ public static boolean isPostureGuidanceShowing(@DevicePostureInt int devicePosture,
+ boolean isLaunchedPostureGuidance) {
+ return !isPostureAllowEnrollment(devicePosture) && isLaunchedPostureGuidance;
+ }
+
+ /**
+ * Used to check if current device posture state is allow to enroll biometrics.
+ * For compatibility, we don't restrict enrollment if device do not config.
+ *
+ * @param devicePosture True if current device posture allow enrollment
+ * @return True if current device posture state allow enrollment
+ */
+ public static boolean isPostureAllowEnrollment(@DevicePostureInt int devicePosture) {
+ return (sAllowEnrollPosture == DEVICE_POSTURE_UNKNOWN)
+ || (devicePosture == sAllowEnrollPosture);
+ }
+
+ /**
+ * Used to check if the activity should show a posture guidance to user.
+ *
+ * @param devicePosture the device posture state
+ * @param isLaunchedPostureGuidance True launching a posture guidance to user
+ * @return True if posture disallow enroll and posture guidance not showing, false otherwise.
+ */
+ public static boolean shouldShowPostureGuidance(@DevicePostureInt int devicePosture,
+ boolean isLaunchedPostureGuidance) {
+ return !isPostureAllowEnrollment(devicePosture) && !isLaunchedPostureGuidance;
+ }
+
+ /**
+ * Sets allowed device posture for face enrollment.
+ *
+ * @param devicePosture the allowed posture state {@link DevicePostureInt} for enrollment
+ */
+ public static void setDevicePosturesAllowEnroll(@DevicePostureInt int devicePosture) {
+ sAllowEnrollPosture = devicePosture;
+ }
+
public static void copyMultiBiometricExtras(@NonNull Intent fromIntent,
@NonNull Intent toIntent) {
PendingIntent pendingIntent = (PendingIntent) fromIntent.getExtra(
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollEducation.java b/src/com/android/settings/biometrics/face/FaceEnrollEducation.java
index d2d356b1104..4ef47522fa8 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollEducation.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollEducation.java
@@ -16,24 +16,35 @@
package com.android.settings.biometrics.face;
+import static com.android.settings.biometrics.BiometricUtils.isPostureAllowEnrollment;
+import static com.android.settings.biometrics.BiometricUtils.isPostureGuidanceShowing;
+
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.res.Configuration;
import android.hardware.face.FaceManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
+import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.Button;
import android.widget.CompoundButton;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.password.SetupSkipDialog;
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
+import com.android.systemui.unfold.updates.FoldProvider;
import com.airbnb.lottie.LottieAnimationView;
import com.google.android.setupcompat.template.FooterBarMixin;
@@ -41,18 +52,19 @@ import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupcompat.util.WizardManagerHelper;
import com.google.android.setupdesign.view.IllustrationVideoView;
+/**
+ * Provides animated education for users to know how to enroll a face with appropriate posture.
+ */
public class FaceEnrollEducation extends BiometricEnrollBase {
private static final String TAG = "FaceEducation";
private FaceManager mFaceManager;
private FaceEnrollAccessibilityToggle mSwitchDiversity;
-
private boolean mIsUsingLottie;
private IllustrationVideoView mIllustrationDefault;
private LottieAnimationView mIllustrationLottie;
private View mIllustrationAccessibility;
private Intent mResultIntent;
- private boolean mNextClicked;
private boolean mAccessibilityEnabled;
private final CompoundButton.OnCheckedChangeListener mSwitchDiversityListener =
@@ -154,6 +166,34 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
}
}
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getPostureGuidanceIntent() == null) {
+ Log.d(TAG, "Device do not support posture guidance");
+ return;
+ }
+
+ BiometricUtils.setDevicePosturesAllowEnroll(
+ getResources().getInteger(R.integer.config_face_enroll_supported_posture));
+
+ if (getPostureCallback() == null) {
+ mFoldCallback = isFolded -> {
+ mDevicePostureState = isFolded ? BiometricUtils.DEVICE_POSTURE_CLOSED
+ : BiometricUtils.DEVICE_POSTURE_OPENED;
+ if (BiometricUtils.shouldShowPostureGuidance(mDevicePostureState,
+ mLaunchedPostureGuidance) && !mNextLaunched) {
+ launchPostureGuidance();
+ }
+ };
+ }
+
+ if (mScreenSizeFoldProvider == null) {
+ mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext());
+ mScreenSizeFoldProvider.registerCallback(mFoldCallback, getMainExecutor());
+ }
+ }
+
@Override
protected void onResume() {
super.onResume();
@@ -172,7 +212,8 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
@Override
protected boolean shouldFinishWhenBackgrounded() {
- return super.shouldFinishWhenBackgrounded() && !mNextClicked;
+ return super.shouldFinishWhenBackgrounded() && !mNextLaunched
+ && !isPostureGuidanceShowing(mDevicePostureState, mLaunchedPostureGuidance);
}
@Override
@@ -206,13 +247,14 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
FaceEnrollAccessibilityDialog dialog = FaceEnrollAccessibilityDialog.newInstance();
dialog.setPositiveButtonListener((dialog1, which) -> {
startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
- mNextClicked = true;
+ mNextLaunched = true;
});
dialog.show(getSupportFragmentManager(), FaceEnrollAccessibilityDialog.class.getName());
} else {
startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
- mNextClicked = true;
+ mNextLaunched = true;
}
+
}
protected void onSkipButtonClick(View view) {
@@ -223,15 +265,29 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
}
}
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mScreenSizeFoldProvider != null && getPostureCallback() != null) {
+ mScreenSizeFoldProvider.onConfigurationChange(newConfig);
+ }
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_POSTURE_GUIDANCE) {
+ mLaunchedPostureGuidance = false;
+ if (resultCode == RESULT_CANCELED || resultCode == RESULT_SKIP) {
+ onSkipButtonClick(getCurrentFocus());
+ }
+ return;
+ }
mResultIntent = data;
boolean hasEnrolledFace = false;
if (data != null) {
hasEnrolledFace = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FACE, false);
}
- if (resultCode == RESULT_TIMEOUT) {
+ if (resultCode == RESULT_TIMEOUT || !isPostureAllowEnrollment(mDevicePostureState)) {
setResult(resultCode, data);
finish();
} else if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST
@@ -243,6 +299,26 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
finish();
}
}
+ mNextLaunched = false;
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected Intent getPostureGuidanceIntent() {
+ return mPostureGuidanceIntent;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected FoldProvider.FoldCallback getPostureCallback() {
+ return mFoldCallback;
+ }
+
+ @VisibleForTesting
+ @BiometricUtils.DevicePostureInt
+ protected int getDevicePostureState() {
+ return mDevicePostureState;
}
@Override
@@ -262,8 +338,10 @@ public class FaceEnrollEducation extends BiometricEnrollBase {
private void showDefaultIllustration() {
if (mIsUsingLottie) {
+ mIllustrationLottie.setAnimation(R.raw.face_education_lottie);
mIllustrationLottie.setVisibility(View.VISIBLE);
mIllustrationLottie.playAnimation();
+ mIllustrationLottie.setProgress(0f);
} else {
mIllustrationDefault.setVisibility(View.VISIBLE);
mIllustrationDefault.start();
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index 0f8bb4355ac..a1233086926 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -23,6 +23,7 @@ import static com.android.settings.biometrics.BiometricUtils.GatekeeperCredentia
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.Intent;
+import android.content.res.Configuration;
import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.face.FaceManager;
@@ -50,6 +51,8 @@ import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.password.SetupSkipDialog;
import com.android.settings.utils.SensorPrivacyManagerHelper;
import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
+import com.android.systemui.unfold.updates.FoldProvider;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupcompat.util.WizardManagerHelper;
@@ -99,6 +102,12 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
}
}
+ @Override
+ protected boolean shouldFinishWhenBackgrounded() {
+ return super.shouldFinishWhenBackgrounded() && !BiometricUtils.isPostureGuidanceShowing(
+ mDevicePostureState, mLaunchedPostureGuidance);
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -188,14 +197,76 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
return Utils.getFaceManagerOrNull(this);
}
+ @VisibleForTesting
+ @Nullable
+ protected Intent getPostureGuidanceIntent() {
+ return mPostureGuidanceIntent;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected FoldProvider.FoldCallback getPostureCallback() {
+ return mFoldCallback;
+ }
+
+ @VisibleForTesting
+ @BiometricUtils.DevicePostureInt
+ protected int getDevicePostureState() {
+ return mDevicePostureState;
+ }
+
@VisibleForTesting
@Nullable
protected byte[] requestGatekeeperHat(long challenge) {
return BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
}
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mScreenSizeFoldProvider != null && getPostureCallback() != null) {
+ mScreenSizeFoldProvider.onConfigurationChange(newConfig);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (getPostureGuidanceIntent() == null) {
+ Log.d(TAG, "Device do not support posture guidance");
+ return;
+ }
+
+ BiometricUtils.setDevicePosturesAllowEnroll(
+ getResources().getInteger(R.integer.config_face_enroll_supported_posture));
+
+ if (getPostureCallback() == null) {
+ mFoldCallback = isFolded -> {
+ mDevicePostureState = isFolded ? BiometricUtils.DEVICE_POSTURE_CLOSED
+ : BiometricUtils.DEVICE_POSTURE_OPENED;
+ if (BiometricUtils.shouldShowPostureGuidance(mDevicePostureState,
+ mLaunchedPostureGuidance) && !mNextLaunched) {
+ launchPostureGuidance();
+ }
+ };
+ }
+
+ if (mScreenSizeFoldProvider == null) {
+ mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext());
+ mScreenSizeFoldProvider.registerCallback(mFoldCallback, getMainExecutor());
+ }
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_POSTURE_GUIDANCE) {
+ mLaunchedPostureGuidance = false;
+ if (resultCode == RESULT_CANCELED || resultCode == RESULT_SKIP) {
+ onSkipButtonClick(getCurrentFocus());
+ }
+ return;
+ }
+
// If user has skipped or finished enrolling, don't restart enrollment.
final boolean isEnrollRequest = requestCode == BIOMETRIC_FIND_SENSOR_REQUEST
|| requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST;
@@ -206,10 +277,12 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
hasEnrolledFace = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FACE, false);
}
- if (resultCode == RESULT_CANCELED && hasEnrolledFace) {
- setResult(resultCode, data);
- finish();
- return;
+ if (resultCode == RESULT_CANCELED) {
+ if (hasEnrolledFace || !BiometricUtils.isPostureAllowEnrollment(mDevicePostureState)) {
+ setResult(resultCode, data);
+ finish();
+ return;
+ }
}
if (isEnrollRequest && isResultSkipOrFinished || hasEnrolledFace) {
diff --git a/src/com/android/settings/biometrics/face/FaceFeatureProvider.java b/src/com/android/settings/biometrics/face/FaceFeatureProvider.java
index cabb528c24e..1a4fd90668e 100644
--- a/src/com/android/settings/biometrics/face/FaceFeatureProvider.java
+++ b/src/com/android/settings/biometrics/face/FaceFeatureProvider.java
@@ -17,9 +17,16 @@
package com.android.settings.biometrics.face;
import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
/** Feature provider for face unlock */
public interface FaceFeatureProvider {
+ /** Returns specified intent config by resource R.string.config_face_enroll_guidance_page. */
+ @Nullable
+ Intent getPostureGuidanceIntent(Context context);
+
/** Returns true if attention checking is supported. */
boolean isAttentionSupported(Context context);
diff --git a/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java b/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java
index 0c8709b0c56..8b7edce40fb 100644
--- a/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java
+++ b/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java
@@ -16,12 +16,41 @@
package com.android.settings.biometrics.face;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
public class FaceFeatureProviderImpl implements FaceFeatureProvider {
+ /**
+ * Returns the guidance page intent if device support {@link FoldingFeature}, and we want to
+ * guide user enrolling faces with specific device posture.
+ *
+ * @param context the application context
+ * @return the posture guidance intent, otherwise null if device not support
+ */
+ @Nullable
+ @Override
+ public Intent getPostureGuidanceIntent(Context context) {
+ final String flattenedString = context.getString(R.string.config_face_enroll_guidance_page);
+ final Intent intent;
+ if (!TextUtils.isEmpty(flattenedString)) {
+ ComponentName componentName = ComponentName.unflattenFromString(flattenedString);
+ if (componentName != null) {
+ intent = new Intent();
+ intent.setComponent(componentName);
+ return intent;
+ }
+ }
+ return null;
+ }
+
@Override
public boolean isAttentionSupported(Context context) {
return true;
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollEducationTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollEducationTest.java
new file mode 100644
index 00000000000..b4ddddedacc
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollEducationTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.face;
+
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+import static android.util.DisplayMetrics.DENSITY_XXXHIGH;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_NEXT_LAUNCHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_LAUNCHED_POSTURE_GUIDANCE;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_CLOSED;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_OPENED;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.hardware.face.FaceManager;
+import android.view.View;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowUtils;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowUtils.class})
+public class FaceEnrollEducationTest {
+ @Mock
+ private FaceManager mFaceManager;
+
+ private Context mContext;
+ private ActivityController mActivityController;
+ private TestFaceEnrollEducation mActivity;
+ private FakeFeatureFactory mFakeFeatureFactory;
+
+ public static class TestFaceEnrollEducation extends FaceEnrollEducation {
+
+ @Override
+ protected boolean launchPostureGuidance() {
+ return super.launchPostureGuidance();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowUtils.setFaceManager(mFaceManager);
+ mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowUtils.reset();
+ }
+
+ private void setupActivityForPosture() {
+ final Intent testIntent = new Intent();
+ // Set the challenge token so the confirm screen will not be shown
+ testIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]);
+ testIntent.putExtra(EXTRA_KEY_NEXT_LAUNCHED, false);
+ testIntent.putExtra(EXTRA_LAUNCHED_POSTURE_GUIDANCE, false);
+
+ when(mFakeFeatureFactory.mFaceFeatureProvider.getPostureGuidanceIntent(any())).thenReturn(
+ testIntent);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mActivityController = Robolectric.buildActivity(
+ TestFaceEnrollEducation.class, testIntent);
+ mActivity = spy(mActivityController.create().get());
+
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+ }
+
+ private void setupActivity() {
+ final Intent testIntent = new Intent();
+ // Set the challenge token so the confirm screen will not be shown
+ testIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]);
+
+ when(mFakeFeatureFactory.mFaceFeatureProvider.getPostureGuidanceIntent(any())).thenReturn(
+ null /* Simulate no posture intent */);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mActivityController = Robolectric.buildActivity(
+ TestFaceEnrollEducation.class, testIntent);
+ mActivity = spy(mActivityController.create().get());
+
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+ }
+
+ private GlifLayout getGlifLayout() {
+ return mActivity.findViewById(R.id.setup_wizard_layout);
+ }
+
+ @Test
+ public void testFaceEnrollEducation_hasHeader() {
+ setupActivity();
+ CharSequence headerText = getGlifLayout().getHeaderText();
+
+ assertThat(headerText.toString()).isEqualTo(
+ mContext.getString(R.string.security_settings_face_enroll_education_title));
+ }
+
+ @Test
+ public void testFaceEnrollEducation_hasDescription() {
+ setupActivity();
+ CharSequence desc = getGlifLayout().getDescriptionText();
+
+ assertThat(desc.toString()).isEqualTo(
+ mContext.getString(R.string.security_settings_face_enroll_education_message));
+ }
+
+ @Test
+ public void testFaceEnrollEducation_showFooterPrimaryButton() {
+ setupActivity();
+ FooterBarMixin footer = getGlifLayout().getMixin(FooterBarMixin.class);
+ FooterButton footerButton = footer.getPrimaryButton();
+
+ assertThat(footerButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(footerButton.getText().toString()).isEqualTo(
+ mContext.getString(R.string.security_settings_face_enroll_education_start));
+ }
+
+ @Test
+ public void testFaceEnrollEducation_showFooterSecondaryButton() {
+ setupActivity();
+ FooterBarMixin footer = getGlifLayout().getMixin(FooterBarMixin.class);
+ FooterButton footerButton = footer.getSecondaryButton();
+
+ assertThat(footerButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(footerButton.getText().toString()).isEqualTo(mContext.getString(
+ R.string.security_settings_face_enroll_introduction_cancel));
+ }
+
+ @Test
+ public void testFaceEnrollEducation_defaultNeverLaunchPostureGuidance() {
+ setupActivity();
+
+ assertThat(mActivity.launchPostureGuidance()).isFalse();
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_UNKNOWN);
+ }
+
+ @Test
+ public void testFaceEnrollEducation_onStartNeverRegisterPostureChangeCallback() {
+ setupActivity();
+ mActivity.onStart();
+
+ assertThat(mActivity.getPostureGuidanceIntent()).isNull();
+ assertThat(mActivity.getPostureCallback()).isNull();
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_UNKNOWN);
+ }
+
+ @Test
+ public void testFaceEnrollEducationWithPosture_onStartRegisteredPostureChangeCallback() {
+ setupActivityForPosture();
+ mActivity.onStart();
+
+ assertThat(mActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mActivity.getPostureCallback()).isNotNull();
+ }
+
+ @Test
+ public void testFaceEnrollEducationWithPosture_onFoldedUpdated_unFolded() {
+ final Configuration newConfig = new Configuration();
+ newConfig.smallestScreenWidthDp = DENSITY_XXXHIGH;
+ setupActivityForPosture();
+ mActivity.onStart();
+
+ assertThat(mActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mActivity.getPostureCallback()).isNotNull();
+
+ mActivity.onConfigurationChanged(newConfig);
+
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_OPENED);
+ }
+
+ @Test
+ public void testFaceEnrollEducationWithPosture_onFoldedUpdated_folded() {
+ final Configuration newConfig = new Configuration();
+ newConfig.smallestScreenWidthDp = DENSITY_DEFAULT;
+ setupActivityForPosture();
+ mActivity.onStart();
+
+ assertThat(mActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mActivity.getPostureCallback()).isNotNull();
+
+ mActivity.onConfigurationChanged(newConfig);
+
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_CLOSED);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
index 2e5cc024d3b..6c04add9261 100644
--- a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java
@@ -16,29 +16,61 @@
package com.android.settings.biometrics.face;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
+import static android.util.DisplayMetrics.DENSITY_XXXHIGH;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_NEXT_LAUNCHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_LAUNCHED_POSTURE_GUIDANCE;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_CLOSED;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_OPENED;
+import static com.android.settings.biometrics.BiometricUtils.DEVICE_POSTURE_UNKNOWN;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
+import android.hardware.face.Face;
import android.hardware.face.FaceManager;
+import android.os.UserHandle;
+import android.view.View;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.R;
import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.password.ChooseLockSettingsHelper;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowDevicePolicyManager;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import com.android.settings.testutils.shadow.ShadowSensorPrivacyManager;
import com.android.settings.testutils.shadow.ShadowUserManager;
+import com.android.settings.testutils.shadow.ShadowUtils;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.view.BottomScrollView;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
@@ -47,69 +79,30 @@ import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowActivity;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {
ShadowLockPatternUtils.class,
ShadowUserManager.class,
+ ShadowUtils.class,
+ ShadowDevicePolicyManager.class,
ShadowSensorPrivacyManager.class
})
public class FaceEnrollIntroductionTest {
- @Mock private FaceManager mFaceManager;
+ @Mock
+ private FaceManager mFaceManager;
+ @Mock
+ private LockPatternUtils mLockPatternUtils;
- private ActivityController mController;
+ private Context mContext;
+ private ActivityController extends Activity> mController;
private TestFaceEnrollIntroduction mActivity;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- }
-
- private void setupActivity(@NonNull Intent intent) {
- doAnswer(invocation -> {
- final FaceManager.GenerateChallengeCallback callback =
- invocation.getArgument(1);
- callback.onGenerateChallengeResult(0, 0, 1L);
- return null;
- }).when(mFaceManager).generateChallenge(anyInt(), any());
- mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, intent);
- mActivity = mController.get();
- mActivity.mOverrideFaceManager = mFaceManager;
- }
-
- @Test
- public void testOnCreate() {
- setupActivity(new Intent());
- mController.create();
- }
-
- @Test
- public void testOnCreateToGenerateChallenge() {
- setupActivity(new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
- mActivity.mGateKeeperAction = GateKeeperAction.RETURN_BYTE_ARRAY;
- mController.create();
- }
-
- @Test
- public void testGenerateChallengeFailThenRecreate() {
- setupActivity(new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
- mActivity.mGateKeeperAction = GateKeeperAction.THROW_CREDENTIAL_NOT_MATCH;
- mController.create();
-
- // Make sure recreate() is called on original activity
- assertThat(mActivity.getRecreateCount()).isEqualTo(1);
-
- // Simulate recreate() action
- setupActivity(mActivity.getIntent());
- mController.create();
-
- // Verify confirmLock()
- assertThat(mActivity.getConfirmingCredentials()).isTrue();
- ShadowActivity shadowActivity = Shadows.shadowOf(mActivity);
- ShadowActivity.IntentForResult startedActivity =
- shadowActivity.getNextStartedActivityForResult();
- assertWithMessage("Next activity 1").that(startedActivity).isNotNull();
- }
+ private FaceEnrollIntroduction mSpyActivity;
+ private FakeFeatureFactory mFakeFeatureFactory;
+ private ShadowUserManager mUserManager;
enum GateKeeperAction { CALL_SUPER, RETURN_BYTE_ARRAY, THROW_CREDENTIAL_NOT_MATCH }
@@ -132,14 +125,15 @@ public class FaceEnrollIntroductionTest {
}
public FaceManager mOverrideFaceManager = null;
- @NonNull public GateKeeperAction mGateKeeperAction = GateKeeperAction.CALL_SUPER;
+ @NonNull
+ public GateKeeperAction mGateKeeperAction = GateKeeperAction.CALL_SUPER;
@Nullable
@Override
public byte[] requestGatekeeperHat(long challenge) {
switch (mGateKeeperAction) {
case RETURN_BYTE_ARRAY:
- return new byte[] { 1 };
+ return new byte[]{1};
case THROW_CREDENTIAL_NOT_MATCH:
throw new BiometricUtils.GatekeeperCredentialNotMatchException("test");
case CALL_SUPER:
@@ -153,5 +147,257 @@ public class FaceEnrollIntroductionTest {
protected FaceManager getFaceManager() {
return mOverrideFaceManager;
}
+
+ @Override
+ protected boolean launchPostureGuidance() {
+ return super.launchPostureGuidance();
+ }
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowUtils.setFaceManager(mFaceManager);
+ mUserManager = ShadowUserManager.getShadow();
+ mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
+
+ when(mFakeFeatureFactory.securityFeatureProvider.getLockPatternUtils(any(Context.class)))
+ .thenReturn(mLockPatternUtils);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowUtils.reset();
+ }
+
+ private void setupActivity() {
+ final Intent testIntent = new Intent();
+ // Set the challenge token so the confirm screen will not be shown
+ testIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]);
+
+ when(mFakeFeatureFactory.mFaceFeatureProvider.getPostureGuidanceIntent(any())).thenReturn(
+ null /* Simulate no posture intent */);
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mUserManager.addUserProfile(new UserHandle(0));
+ mController = Robolectric.buildActivity(
+ TestFaceEnrollIntroduction.class, testIntent);
+ mActivity = (TestFaceEnrollIntroduction) spy(mController.get());
+ mActivity.mOverrideFaceManager = mFaceManager;
+ when(mActivity.getPostureGuidanceIntent()).thenReturn(null);
+ when(mContext.getApplicationContext()).thenReturn(mContext);
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+ when(mLockPatternUtils.getActivePasswordQuality(Mockito.anyInt())).thenReturn(
+ PASSWORD_QUALITY_NUMERIC);
+
+ mController.create();
+ }
+
+ private void setupActivityForPosture() {
+ final Intent testIntent = new Intent();
+ // Set the challenge token so the confirm screen will not be shown
+ testIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, new byte[0]);
+ testIntent.putExtra(EXTRA_KEY_NEXT_LAUNCHED, false);
+ testIntent.putExtra(EXTRA_LAUNCHED_POSTURE_GUIDANCE, false);
+
+ when(mFakeFeatureFactory.mFaceFeatureProvider.getPostureGuidanceIntent(any())).thenReturn(
+ testIntent);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mUserManager.addUserProfile(new UserHandle(0));
+ mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, testIntent);
+ mSpyActivity = (FaceEnrollIntroduction) spy(mController.get());
+ when(mSpyActivity.getPostureGuidanceIntent()).thenReturn(testIntent);
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+ when(mLockPatternUtils.getActivePasswordQuality(Mockito.anyInt())).thenReturn(
+ PASSWORD_QUALITY_NUMERIC);
+
+ mController.create();
+ }
+
+ private void setupActivityWithGenerateChallenge(@NonNull Intent intent) {
+ doAnswer(invocation -> {
+ final FaceManager.GenerateChallengeCallback callback =
+ invocation.getArgument(1);
+ callback.onGenerateChallengeResult(0, 0, 1L);
+ return null;
+ }).when(mFaceManager).generateChallenge(anyInt(), any());
+ mController = Robolectric.buildActivity(TestFaceEnrollIntroduction.class, intent);
+ mActivity = (TestFaceEnrollIntroduction) mController.get();
+ mActivity.mOverrideFaceManager = mFaceManager;
+ }
+
+ private GlifLayout getGlifLayout(Activity activity) {
+ return activity.findViewById(R.id.setup_wizard_layout);
+ }
+
+ private void setFaceManagerToHave(int numEnrollments) {
+ List faces = new ArrayList<>();
+ for (int i = 0; i < numEnrollments; i++) {
+ faces.add(new Face("Face " + i /* name */, 1 /*faceId */, 1 /* deviceId */));
+ }
+ when(mFaceManager.getEnrolledFaces(anyInt())).thenReturn(faces);
+ }
+
+ @Test
+ public void intro_CheckCanEnroll() {
+ setFaceManagerToHave(0 /* numEnrollments */);
+ setupActivityWithGenerateChallenge(new Intent());
+ mController.create();
+ int result = mActivity.checkMaxEnrolled();
+
+ assertThat(result).isEqualTo(0);
+ }
+
+ @Test
+ public void intro_CheckMaxEnrolled() {
+ setFaceManagerToHave(1 /* numEnrollments */);
+ setupActivityWithGenerateChallenge(new Intent());
+ mController.create();
+ int result = mActivity.checkMaxEnrolled();
+
+ assertThat(result).isEqualTo(R.string.face_intro_error_max);
+ }
+
+ @Test
+ public void testOnCreate() {
+ setupActivityWithGenerateChallenge(new Intent());
+ mController.create();
+ }
+
+ @Test
+ public void testOnCreateToGenerateChallenge() {
+ setupActivityWithGenerateChallenge(
+ new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
+ mActivity.mGateKeeperAction = GateKeeperAction.RETURN_BYTE_ARRAY;
+ mController.create();
+ }
+
+ @Test
+ public void testGenerateChallengeFailThenRecreate() {
+ setupActivityWithGenerateChallenge(
+ new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L));
+ mActivity.mGateKeeperAction = GateKeeperAction.THROW_CREDENTIAL_NOT_MATCH;
+ mController.create();
+
+ // Make sure recreate() is called on original activity
+ assertThat(mActivity.getRecreateCount()).isEqualTo(1);
+
+ // Simulate recreate() action
+ setupActivityWithGenerateChallenge(mActivity.getIntent());
+ mController.create();
+
+ // Verify confirmLock()
+ assertThat(mActivity.getConfirmingCredentials()).isTrue();
+ ShadowActivity shadowActivity = Shadows.shadowOf(mActivity);
+ ShadowActivity.IntentForResult startedActivity =
+ shadowActivity.getNextStartedActivityForResult();
+ assertWithMessage("Next activity 1").that(startedActivity).isNotNull();
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_hasHeader() {
+ setupActivity();
+ TextView headerTextView = getGlifLayout(mActivity).findViewById(R.id.suc_layout_title);
+
+ assertThat(headerTextView).isNotNull();
+ assertThat(headerTextView.getText().toString()).isNotEmpty();
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_hasDescription() {
+ setupActivity();
+ CharSequence desc = getGlifLayout(mActivity).getDescriptionText();
+
+ assertThat(desc.toString()).isEqualTo(
+ mContext.getString(R.string.security_settings_face_enroll_introduction_message));
+ }
+
+ @Test
+ public void testFaceEnrollEducation_hasBottomScrollView() {
+ setupActivity();
+ BottomScrollView scrollView = getGlifLayout(mActivity).findViewById(R.id.sud_scroll_view);
+
+ assertThat(scrollView).isNotNull();
+ assertThat(scrollView.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_showFooterPrimaryButton() {
+ setupActivity();
+ FooterBarMixin footer = getGlifLayout(mActivity).getMixin(FooterBarMixin.class);
+ FooterButton footerButton = footer.getPrimaryButton();
+
+ assertThat(footerButton).isNotNull();
+ assertThat(footerButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(footerButton.getText().toString()).isEqualTo(
+ mContext.getString(R.string.security_settings_face_enroll_introduction_agree));
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_notShowFooterSecondaryButton() {
+ setupActivity();
+ FooterBarMixin footer = getGlifLayout(mActivity).getMixin(FooterBarMixin.class);
+ FooterButton footerButton = footer.getSecondaryButton();
+
+ assertThat(footerButton.getVisibility()).isEqualTo(View.INVISIBLE);
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_defaultNeverLaunchPostureGuidance() {
+ setupActivity();
+
+ assertThat(mActivity.launchPostureGuidance()).isFalse();
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_UNKNOWN);
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_onStartNeverRegisterPostureChangeCallback() {
+ setupActivity();
+ mActivity.onStart();
+
+ assertThat(mActivity.getPostureGuidanceIntent()).isNull();
+ assertThat(mActivity.getPostureCallback()).isNull();
+ assertThat(mActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_UNKNOWN);
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_onStartRegisteredPostureChangeCallback() {
+ setupActivityForPosture();
+ mSpyActivity.onStart();
+
+ assertThat(mSpyActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mSpyActivity.getPostureCallback()).isNotNull();
+ }
+
+ @Test
+ public void testFaceEnrollIntroduction_onFoldedUpdated_unFolded() {
+ final Configuration newConfig = new Configuration();
+ newConfig.smallestScreenWidthDp = DENSITY_XXXHIGH;
+ setupActivityForPosture();
+ mSpyActivity.onStart();
+
+ assertThat(mSpyActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mSpyActivity.getPostureCallback()).isNotNull();
+
+ mSpyActivity.onConfigurationChanged(newConfig);
+
+ assertThat(mSpyActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_OPENED);
+ }
+
+ @Test
+ public void testFaceEnrollEducation_onFoldedUpdated_folded() {
+ final Configuration newConfig = new Configuration();
+ newConfig.smallestScreenWidthDp = DENSITY_DEFAULT;
+ setupActivityForPosture();
+ mSpyActivity.onStart();
+
+ assertThat(mSpyActivity.getPostureGuidanceIntent()).isNotNull();
+ assertThat(mSpyActivity.getPostureCallback()).isNotNull();
+
+ mSpyActivity.onConfigurationChanged(newConfig);
+
+ assertThat(mSpyActivity.getDevicePostureState()).isEqualTo(DEVICE_POSTURE_CLOSED);
}
}