Merge changes from topics "disable_face_enroll_inSUW", "tuscany_FaceEnrollFoldPage" into tm-qpr-dev

* changes:
  3-1/ Impl FoldProvider.FoldCallback for Face enroll activities
  2-1/ Add config_suw_support_face_enroll to customize SUW face enroll flow
  Fix face enroll introduction crash after 10mins
This commit is contained in:
Bill Lin
2023-01-12 06:47:45 +00:00
committed by Android (Google) Code Review
13 changed files with 1035 additions and 35 deletions

View File

@@ -283,6 +283,19 @@
<!-- ComponentName to launch a vendor-specific enrollment activity if available -->
<string name="config_face_enroll" translatable="false"></string>
<!-- ComponentName to launch a vendor-specific posture guidance activity if available -->
<string name="config_face_enroll_guidance_page" translatable="false"></string>
<!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN)
means setting will try listening on device posture changes.
0 : DEVICE_POSTURE_UNKNOWN
1 : DEVICE_POSTURE_CLOSED
2 : DEVICE_POSTURE_HALF_OPENED
3 : DEVICE_POSTURE_OPENED
4 : DEVICE_POSTURE_FLIPPED
-->
<integer name="config_face_enroll_supported_posture">0</integer>
<!-- Whether to show the "less secure" info section on the face enroll intro screen -->
<bool name="config_face_intro_show_less_secure">false</bool>
@@ -292,6 +305,9 @@
<!-- Whether to use the Lottie animation for the face education enrollment screen -->
<bool name="config_face_education_use_lottie">false</bool>
<!-- Whether to support enrollment during setup wizard flow -->
<bool name="config_suw_support_face_enroll">true</bool>
<!-- App intent -->
<string name="config_account_intent_uri" translatable="false"></string>

View File

@@ -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 */));
}

View File

@@ -48,6 +48,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.settings.R;
import com.android.settings.SetupWizardUtils;
import com.android.settings.core.InstrumentedActivity;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockPattern;
import com.android.settings.password.ChooseLockSettingsHelper;
@@ -215,11 +216,16 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
mIsFaceEnrollable =
faceManager.getEnrolledFaces(mUserId).size() < maxEnrolls;
// exclude face enrollment from setup wizard if configured as a convenience
// isSetupWizard is always false for unicorn enrollment, so if consent is
// required check if setup has completed instead.
final boolean isSettingUp = isSetupWizard || (mParentalOptionsRequired
final boolean parentalConsent = isSetupWizard || (mParentalOptionsRequired
&& !WizardManagerHelper.isUserSetupComplete(this));
if (parentalConsent && isMultiSensor && mIsFaceEnrollable) {
// Exclude face enrollment from setup wizard if feature config not supported
// in setup wizard flow, we still allow user enroll faces through settings.
mIsFaceEnrollable = FeatureFactory.getFactory(getApplicationContext())
.getFaceFeatureProvider()
.isSetupWizardSupported(getApplicationContext());
Log.d(TAG, "config_suw_support_face_enroll: " + mIsFaceEnrollable);
}
}
}
if (mHasFeatureFingerprint) {

View File

@@ -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());
}

View File

@@ -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() {

View File

@@ -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,17 +48,53 @@ 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.
*/
public static int REQUEST_ADD_ANOTHER = 7;
/**
* Gatekeeper credential not match exception, it throws if VerifyCredentialResponse is not
* matched in requestGatekeeperHat().
*/
public static class GatekeeperCredentialNotMatchException extends IllegalStateException {
public GatekeeperCredentialNotMatchException(String s) {
super(s);
}
};
/**
* Given the result from confirming or choosing a credential, request Gatekeeper to generate
* a HardwareAuthToken with the Gatekeeper Password together with a biometric challenge.
@@ -67,6 +104,8 @@ public class BiometricUtils {
* @param userId User ID that the credential/biometric operation applies to
* @param challenge Unique biometric challenge from FingerprintManager/FaceManager
* @return
* @throws GatekeeperCredentialNotMatchException if Gatekeeper response is not match
* @throws IllegalStateException if Gatekeeper Password is missing
*/
public static byte[] requestGatekeeperHat(@NonNull Context context, @NonNull Intent result,
int userId, long challenge) {
@@ -84,7 +123,7 @@ public class BiometricUtils {
final VerifyCredentialResponse response = utils.verifyGatekeeperPasswordHandle(gkPwHandle,
challenge, userId);
if (!response.isMatched()) {
throw new IllegalStateException("Unable to request Gatekeeper HAT");
throw new GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT");
}
return response.getGatekeeperHAT();
}
@@ -270,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(

View File

@@ -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();

View File

@@ -18,9 +18,12 @@ package com.android.settings.biometrics.face;
import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_UNLOCK_DISABLED;
import static com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException;
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;
@@ -36,6 +39,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
@@ -43,11 +47,12 @@ import com.android.settings.biometrics.BiometricEnrollActivity;
import com.android.settings.biometrics.BiometricEnrollIntroduction;
import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.biometrics.MultiBiometricEnrollHelper;
import com.android.settings.overlay.FeatureFactory;
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;
@@ -61,7 +66,6 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
private static final String TAG = "FaceEnrollIntroduction";
private FaceManager mFaceManager;
private FaceFeatureProvider mFaceFeatureProvider;
@Nullable private FooterButton mPrimaryFooterButton;
@Nullable private FooterButton mSecondaryFooterButton;
@Nullable private SensorPrivacyManager mSensorPrivacyManager;
@@ -98,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);
@@ -142,9 +152,7 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
infoMessageRequireEyes.setText(getInfoMessageRequireEyes());
}
mFaceManager = Utils.getFaceManagerOrNull(this);
mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext())
.getFaceFeatureProvider();
mFaceManager = getFaceManager();
// This path is an entry point for SetNewPasswordController, e.g.
// adb shell am start -a android.app.action.SET_NEW_PASSWORD
@@ -154,11 +162,22 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
// We either block on generateChallenge, or need to gray out the "next" button until
// the challenge is ready. Let's just do this for now.
mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId,
challenge);
mSensorId = sensorId;
mChallenge = challenge;
mFooterBarMixin.getPrimaryButton().setEnabled(true);
if (isFinishing()) {
// Do nothing if activity is finishing
Log.w(TAG, "activity finished before challenge callback launched.");
return;
}
try {
mToken = requestGatekeeperHat(challenge);
mSensorId = sensorId;
mChallenge = challenge;
mFooterBarMixin.getPrimaryButton().setEnabled(true);
} catch (GatekeeperCredentialNotMatchException e) {
// Let BiometricEnrollBase#onCreate() to trigger confirmLock()
getIntent().removeExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE);
recreate();
}
});
}
}
@@ -172,8 +191,82 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled);
}
@VisibleForTesting
@Nullable
protected FaceManager getFaceManager() {
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;
@@ -184,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) {

View File

@@ -17,9 +17,19 @@
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);
/** Returns true if setup wizard supported face enrollment. */
boolean isSetupWizardSupported(Context context);
}

View File

@@ -16,13 +16,48 @@
package com.android.settings.biometrics.face;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
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;
}
@Override
public boolean isSetupWizardSupported(@NonNull Context context) {
return true;
}
}

View File

@@ -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<TestFaceEnrollEducation> 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);
}
}

View File

@@ -0,0 +1,403 @@
/*
* Copyright (C) 2022 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.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;
import org.robolectric.Shadows;
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 LockPatternUtils mLockPatternUtils;
private Context mContext;
private ActivityController<? extends Activity> mController;
private TestFaceEnrollIntroduction mActivity;
private FaceEnrollIntroduction mSpyActivity;
private FakeFeatureFactory mFakeFeatureFactory;
private ShadowUserManager mUserManager;
enum GateKeeperAction { CALL_SUPER, RETURN_BYTE_ARRAY, THROW_CREDENTIAL_NOT_MATCH }
public static class TestFaceEnrollIntroduction extends FaceEnrollIntroduction {
private int mRecreateCount = 0;
public int getRecreateCount() {
return mRecreateCount;
}
@Override
public void recreate() {
mRecreateCount++;
// Do nothing
}
public boolean getConfirmingCredentials() {
return mConfirmingCredentials;
}
public FaceManager mOverrideFaceManager = null;
@NonNull
public GateKeeperAction mGateKeeperAction = GateKeeperAction.CALL_SUPER;
@Nullable
@Override
public byte[] requestGatekeeperHat(long challenge) {
switch (mGateKeeperAction) {
case RETURN_BYTE_ARRAY:
return new byte[]{1};
case THROW_CREDENTIAL_NOT_MATCH:
throw new BiometricUtils.GatekeeperCredentialNotMatchException("test");
case CALL_SUPER:
default:
return super.requestGatekeeperHat(challenge);
}
}
@Nullable
@Override
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<Face> 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);
}
}