From 8e5780824755ee3983d475a540b20f1a89cc286c Mon Sep 17 00:00:00 2001 From: Milton Wu Date: Mon, 7 Nov 2022 03:33:19 +0000 Subject: [PATCH 1/3] Fix face enroll introduction crash after 10mins When requestGatekeeperHat() throws exception in FaceEnrollIntroduction page, remove gk_pw_handle and recreate activity to trigger confirmLock. Test: robotest for FaceEnrollIntroductionTest Bug: 234437174 Change-Id: Ie1dd6f36e4deb3f776e3b39acd165fc47d04f526 Merged-In: Ie1dd6f36e4deb3f776e3b39acd165fc47d04f526 --- .../settings/biometrics/BiometricUtils.java | 15 +- .../face/FaceEnrollIntroduction.java | 42 +++-- .../face/FaceEnrollIntroductionTest.java | 157 ++++++++++++++++++ 3 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java index 4cd2f790620..db9465be54d 100644 --- a/src/com/android/settings/biometrics/BiometricUtils.java +++ b/src/com/android/settings/biometrics/BiometricUtils.java @@ -57,6 +57,17 @@ public class BiometricUtils { * 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. @@ -66,6 +77,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) { @@ -83,7 +96,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(); } diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java index ed74d2a550d..0f8bb4355ac 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -18,6 +18,8 @@ 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; @@ -36,6 +38,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,7 +46,6 @@ 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; @@ -61,7 +63,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; @@ -142,9 +143,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 +153,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,6 +182,18 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled); } + @VisibleForTesting + @Nullable + protected FaceManager getFaceManager() { + return Utils.getFaceManagerOrNull(this); + } + + @VisibleForTesting + @Nullable + protected byte[] requestGatekeeperHat(long challenge) { + return BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // If user has skipped or finished enrolling, don't restart enrollment. diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java new file mode 100644 index 00000000000..2e5cc024d3b --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java @@ -0,0 +1,157 @@ +/* + * 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 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 android.content.Intent; +import android.hardware.face.FaceManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.biometrics.BiometricUtils; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.testutils.shadow.ShadowLockPatternUtils; +import com.android.settings.testutils.shadow.ShadowSensorPrivacyManager; +import com.android.settings.testutils.shadow.ShadowUserManager; + +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.Shadows; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowLockPatternUtils.class, + ShadowUserManager.class, + ShadowSensorPrivacyManager.class +}) +public class FaceEnrollIntroductionTest { + + @Mock private FaceManager mFaceManager; + + private ActivityController 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(); + } + + 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; + } + } +} From e05697fc65a8e72b00ca1b07a4c2ea23e19f1de1 Mon Sep 17 00:00:00 2001 From: lbill Date: Wed, 14 Dec 2022 03:50:11 +0000 Subject: [PATCH 2/3] 2-1/ Add config_suw_support_face_enroll to customize SUW face enroll flow 1. Add config_suw_support_face_enroll default is TRUE 2. Impl FaceFeatureProvider to obtain the config 3. Overlay config_suw_support_face_enroll by requirements Test: Flash build and manual check if device go through face enroll in SUW Bug: 262469686 Change-Id: I61aa5c818bedfb490f2172a7481f59fda7295c1a Merged-In: I61aa5c818bedfb490f2172a7481f59fda7295c1a --- res/values/config.xml | 3 +++ .../biometrics/BiometricEnrollActivity.java | 14 ++++++++++---- .../biometrics/face/FaceFeatureProvider.java | 3 +++ .../biometrics/face/FaceFeatureProviderImpl.java | 8 +++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/res/values/config.xml b/res/values/config.xml index 4aa41425ce6..cf40b9461ea 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -292,6 +292,9 @@ false + + true + diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java index 932c41073e4..bb16f0bb9b6 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollActivity.java +++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java @@ -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) { diff --git a/src/com/android/settings/biometrics/face/FaceFeatureProvider.java b/src/com/android/settings/biometrics/face/FaceFeatureProvider.java index 26ea2615f1c..cabb528c24e 100644 --- a/src/com/android/settings/biometrics/face/FaceFeatureProvider.java +++ b/src/com/android/settings/biometrics/face/FaceFeatureProvider.java @@ -22,4 +22,7 @@ import android.content.Context; public interface FaceFeatureProvider { /** Returns true if attention checking is supported. */ boolean isAttentionSupported(Context context); + + /** Returns true if setup wizard supported face enrollment. */ + boolean isSetupWizardSupported(Context context); } diff --git a/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java b/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java index e5086007cde..0c8709b0c56 100644 --- a/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java +++ b/src/com/android/settings/biometrics/face/FaceFeatureProviderImpl.java @@ -17,7 +17,8 @@ package com.android.settings.biometrics.face; import android.content.Context; -import android.provider.Settings; + +import androidx.annotation.NonNull; public class FaceFeatureProviderImpl implements FaceFeatureProvider { @@ -25,4 +26,9 @@ public class FaceFeatureProviderImpl implements FaceFeatureProvider { public boolean isAttentionSupported(Context context) { return true; } + + @Override + public boolean isSetupWizardSupported(@NonNull Context context) { + return true; + } } From b54ca63996fe9411460e36743c4ce29363c1cf37 Mon Sep 17 00:00:00 2001 From: lbill Date: Thu, 8 Dec 2022 07:11:23 +0000 Subject: [PATCH 3/3] 3-1/ Impl FoldProvider.FoldCallback for Face enroll activities Create a mechanism to allow OEM config posture guidance with 'config_face_enroll_guidance_page', and customize the config 'config_face_enroll_supported_posture' with standard postures 0 : DEVICE_POSTURE_UNKNOWN 1 : DEVICE_POSTURE_CLOSED 2 : DEVICE_POSTURE_HALF_OPENED 3 : DEVICE_POSTURE_OPENED 4 : DEVICE_POSTURE_FLIPPED For example, if we set 1 for the device, then device only allow to enroll face in closed(folded) state, if device do not in the allow state, we will prompt specific guidance page activity defined in config_face_enroll_guidance_page. At this stage , we only integrate 2 states OPENED/CLOSED through ScreenSizeFoldProvider and register for onFoldUpdated() callback - isFold(DEVICE_POSTURE_CLOSED): finish posture guidance - !isFold(DEVICE_POSTURE_OPENED): launch posture guidance - onActivityResult : reset mOnGuidanceShown false 1. Fix A11y lottie animation bug 2. Impl FoldProvider.FoldCallback 3. Register callback to ScreenSizeFoldProvider 4. Integrate back stack, skip, cancel events - Back key : RESULT_CANCELED - Skip btn : RESULT_SKIP - Posture changed : RESULT_FINISHED 5. Set single instance for relative activities 6. FaceEnrollFoldPage listen for onConfigurationChanged() 7. Add empty face_posture_guidance_lottie.json for overlay Test: atest SettingsGoogleUnitTests Test: m -j SettingsGoogleRoboTests RunSettingsGoogleRoboTests Test: m RunSettingsRoboTests ROBOTEST_FILTER= \ "com.android.settings.biometrics.face.FaceEnrollEducationTest" Test: m RunSettingsRoboTests ROBOTEST_FILTER= \ "com.android.settings.biometrics.face.FaceEnrollIntroductionTest" Test: Manual launch security settings face enroll, unfold device and observe posture guidance showing fullscreen on top Test: Fold device ensure the posture guidance activity finish Bug: 261141826 Fixes: 231908496 Change-Id: Ib9f43f82f7d19f3f187c2f6f8984e76cd843afbc Merged-In: Ib9f43f82f7d19f3f187c2f6f8984e76cd843afbc --- res/raw/face_posture_guidance_lottie.json | 0 res/values/config.xml | 13 + .../ActivityEmbeddingRulesController.java | 3 + .../biometrics/BiometricEnrollBase.java | 58 ++- .../BiometricEnrollIntroduction.java | 3 + .../settings/biometrics/BiometricUtils.java | 71 ++++ .../biometrics/face/FaceEnrollEducation.java | 92 ++++- .../face/FaceEnrollIntroduction.java | 81 +++- .../biometrics/face/FaceFeatureProvider.java | 7 + .../face/FaceFeatureProviderImpl.java | 29 ++ .../face/FaceEnrollEducationTest.java | 225 +++++++++++ .../face/FaceEnrollIntroductionTest.java | 356 +++++++++++++++--- 12 files changed, 864 insertions(+), 74 deletions(-) create mode 100644 res/raw/face_posture_guidance_lottie.json create mode 100644 tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollEducationTest.java 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 cf40b9461ea..f49eff736a5 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 db9465be54d..feff405f4dd 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; @@ -46,12 +47,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. @@ -278,6 +304,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 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); } }