Add ActiveUnlock check when picking preference

Modify BiometricsSettingBase to also track if the hardware is supported
and if the controller is a work profile controller. If the hardware is
supported and active unlock is enabled, non-work profile controllers
will still be displayed.

Test: make RunSettingsRoboTests
Test: manually flip flags on device with active unlock, confirm new
layout used
Bug: 264813302

Change-Id: Idb0e994453d4fd5c078c45f87d5d8cee339053a2
diff --git a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java
index f61f99c..76a23a5 100644
--- a/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.widget.LockPatternUtils;
 import com.android.settings.Utils;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.overlay.FeatureFactory;
 
@@ -37,11 +38,17 @@
     protected final int mProfileChallengeUserId;
 
     private final BiometricNavigationUtils mBiometricNavigationUtils;
+    private final ActiveUnlockStatusUtils mActiveUnlockStatusUtils;
+
+    /**
+     * @return true if the controller should be shown exclusively.
+     */
+    protected abstract boolean isDeviceSupported();
 
     /**
      * @return true if the manager is not null and the hardware is detected.
      */
-    protected abstract boolean isDeviceSupported();
+    protected abstract boolean isHardwareSupported();
 
     /**
      * @return the summary text.
@@ -61,13 +68,21 @@
                 .getLockPatternUtils(context);
         mProfileChallengeUserId = Utils.getManagedProfileId(mUm, mUserId);
         mBiometricNavigationUtils = new BiometricNavigationUtils(getUserId());
+        mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(context);
     }
 
     @Override
     public int getAvailabilityStatus() {
+        if (mActiveUnlockStatusUtils.isAvailable()) {
+            return getAvailabilityStatusWithWorkProfileCheck();
+        }
         if (!isDeviceSupported()) {
             return UNSUPPORTED_ON_DEVICE;
         }
+        return getAvailabilityFromUserSupported();
+    }
+
+    private int getAvailabilityFromUserSupported() {
         if (isUserSupported()) {
             return AVAILABLE;
         } else {
@@ -75,6 +90,21 @@
         }
     }
 
+    // Since this code is flag guarded by mActiveUnlockStatusUtils.isAvailable(), we don't need to
+    // do another check here.
+    private int getAvailabilityStatusWithWorkProfileCheck() {
+        if (!isHardwareSupported()) {
+            // no hardware, never show
+            return UNSUPPORTED_ON_DEVICE;
+        }
+        if (!isDeviceSupported() && isWorkProfileController()) {
+            // hardware supported but work profile, don't show
+            return UNSUPPORTED_ON_DEVICE;
+        }
+        // hardware supported, not work profile, active unlock enabled
+        return getAvailabilityFromUserSupported();
+    }
+
     @Override
     public void updateState(Preference preference) {
         if (!isAvailable()) {
@@ -105,4 +135,11 @@
     protected boolean isUserSupported() {
         return true;
     }
+
+    /**
+     * Returns true if the controller controls is used for work profile.
+     */
+    protected boolean isWorkProfileController() {
+        return false;
+    }
 }
diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java
new file mode 100644
index 0000000..640f08d
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java
@@ -0,0 +1,170 @@
+/*
+ * 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.activeunlock;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.core.BasePreferenceController.AvailabilityStatus;
+
+import java.util.List;
+
+/** Utilities for active unlock details shared between Security Settings and Safety Center. */
+public class ActiveUnlockStatusUtils {
+
+    /** The flag to determining whether active unlock in settings is enabled. */
+    public static final String CONFIG_FLAG_NAME = "active_unlock_in_settings";
+
+    /** Flag value that represents the layout for unlock intent should be used. */
+    public static final String UNLOCK_INTENT_LAYOUT = "unlock_intent_layout";
+
+    /** Flag value that represents the layout for biometric failure should be used. */
+    public static final String BIOMETRIC_FAILURE_LAYOUT = "biometric_failure_layout";
+
+    private static final String ACTIVE_UNLOCK_PROVIDER = "active_unlock_provider";
+    private static final String ACTIVE_UNLOCK_TARGET = "active_unlock_target";
+
+    private static final String TAG = "ActiveUnlockStatusUtils";
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+
+    public ActiveUnlockStatusUtils(@NonNull Context context) {
+        mContext = context;
+        mContentResolver = mContext.getContentResolver();
+    }
+
+    /** Returns whether the active unlock settings entity should be shown. */
+    public boolean isAvailable() {
+        return getAvailability() == BasePreferenceController.AVAILABLE;
+    }
+
+    /**
+     * Returns whether the active unlock layout with the unlock on intent configuration should be
+     * used.
+     */
+    public boolean useUnlockIntentLayout() {
+        return isAvailable() && UNLOCK_INTENT_LAYOUT.equals(getFlagState());
+    }
+
+    /**
+     *
+     * Returns whether the active unlock layout with the unlock on biometric failure configuration
+     * should be used.
+     */
+    public boolean useBiometricFailureLayout() {
+        return isAvailable() && BIOMETRIC_FAILURE_LAYOUT.equals(getFlagState());
+    }
+
+    /**
+     * Returns the authority used to fetch dynamic active unlock content.
+     */
+    @Nullable
+    public String getAuthority() {
+        final String authority = Settings.Secure.getString(
+                mContext.getContentResolver(), ACTIVE_UNLOCK_PROVIDER);
+        if (authority == null) {
+            Log.i(TAG, "authority not set");
+            return null;
+        }
+        final List<PackageInfo> packageInfos =
+                mContext.getPackageManager().getInstalledPackages(
+                        PackageManager.PackageInfoFlags.of(PackageManager.GET_PROVIDERS));
+        for (PackageInfo packageInfo : packageInfos) {
+            final ProviderInfo[] providers = packageInfo.providers;
+            if (providers != null) {
+                for (ProviderInfo provider : providers) {
+                    if (authority.equals(provider.authority) && isSystemApp(provider)) {
+                        return authority;
+                    }
+                }
+            }
+        }
+        Log.e(TAG, "authority not valid");
+        return null;
+    }
+
+    private static boolean isSystemApp(ComponentInfo componentInfo) {
+        final ApplicationInfo applicationInfo = componentInfo.applicationInfo;
+        if (applicationInfo == null) {
+            Log.e(TAG, "application info is null");
+            return false;
+        }
+        return applicationInfo.isSystemApp();
+    }
+
+    /**
+     * Returns the intent used to launch the active unlock activity.
+     */
+    @Nullable
+    public Intent getIntent() {
+        final String targetAction = Settings.Secure.getString(
+                mContentResolver, ACTIVE_UNLOCK_TARGET);
+        if (targetAction == null) {
+            Log.i(TAG, "Target action not set");
+            return null;
+        }
+        final Intent intent = new Intent(targetAction);
+        final ActivityInfo activityInfo = intent.resolveActivityInfo(
+                mContext.getPackageManager(), PackageManager.MATCH_ALL);
+        if (activityInfo == null) {
+            Log.e(TAG, "Target activity not found");
+            return null;
+        }
+        if (!isSystemApp(activityInfo)) {
+            Log.e(TAG, "Target application is not system");
+            return null;
+        }
+        Log.i(TAG, "Target application is valid");
+        return intent;
+    }
+
+    /** Returns the availability status of the active unlock feature. */
+    @AvailabilityStatus
+    int getAvailability() {
+        if (!Utils.hasFingerprintHardware(mContext) && !Utils.hasFaceHardware(mContext)) {
+            return BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+        }
+        if (!UNLOCK_INTENT_LAYOUT.equals(getFlagState())
+                  && !BIOMETRIC_FAILURE_LAYOUT.equals(getFlagState())) {
+            return BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+        }
+        if (getAuthority() != null && getIntent() != null) {
+            return BasePreferenceController.AVAILABLE;
+        }
+        return BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+    }
+
+    private static String getFlagState() {
+        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_REMOTE_AUTH, CONFIG_FLAG_NAME);
+    }
+}
diff --git a/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java
index de02126..c21368b 100644
--- a/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricFaceProfileStatusPreferenceController.java
@@ -46,4 +46,9 @@
     protected int getUserId() {
         return mProfileChallengeUserId;
     }
+
+    @Override
+    protected boolean isWorkProfileController() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java
index 800139c..c9ea944 100644
--- a/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceController.java
@@ -39,6 +39,11 @@
 
     @Override
     protected boolean isDeviceSupported() {
-        return Utils.isMultipleBiometricsSupported(mContext) && Utils.hasFaceHardware(mContext);
+        return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported();
+    }
+
+    @Override
+    protected boolean isHardwareSupported() {
+        return Utils.hasFaceHardware(mContext);
     }
 }
diff --git a/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java
index 0c50230..52e4431 100644
--- a/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricFingerprintProfileStatusPreferenceController.java
@@ -46,4 +46,9 @@
     protected int getUserId() {
         return mProfileChallengeUserId;
     }
+
+    @Override
+    protected boolean isWorkProfileController() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java
index be19cb5..9789417 100644
--- a/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceController.java
@@ -40,7 +40,11 @@
 
     @Override
     protected boolean isDeviceSupported() {
-        return Utils.isMultipleBiometricsSupported(mContext)
-                && Utils.hasFingerprintHardware(mContext);
+        return Utils.isMultipleBiometricsSupported(mContext) && isHardwareSupported();
+    }
+
+    @Override
+    protected boolean isHardwareSupported() {
+        return Utils.hasFingerprintHardware(mContext);
     }
 }
diff --git a/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java
index a46ae7a..6153a1a 100644
--- a/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricSettingsAppPreferenceController.java
@@ -24,6 +24,7 @@
 import android.provider.Settings;
 
 import com.android.settings.Utils;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
 import com.android.settings.core.TogglePreferenceController;
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -69,7 +70,10 @@
 
     @Override
     public int getAvailabilityStatus() {
-        if (!Utils.isMultipleBiometricsSupported(mContext)) {
+        final ActiveUnlockStatusUtils activeUnlockStatusUtils =
+                new ActiveUnlockStatusUtils(mContext);
+        if (!Utils.isMultipleBiometricsSupported(mContext)
+                && !activeUnlockStatusUtils.isAvailable()) {
             return UNSUPPORTED_ON_DEVICE;
         }
         if (mFaceManager == null || mFingerprintManager == null) {
diff --git a/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java b/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java
index 2d22558..cfd220e 100644
--- a/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/BiometricSettingsKeyguardPreferenceController.java
@@ -22,6 +22,7 @@
 import android.provider.Settings;
 
 import com.android.settings.Utils;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
 import com.android.settings.core.TogglePreferenceController;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -63,9 +64,18 @@
 
     @Override
     public int getAvailabilityStatus() {
+        final ActiveUnlockStatusUtils activeUnlockStatusUtils =
+                new ActiveUnlockStatusUtils(mContext);
+        if (activeUnlockStatusUtils.isAvailable()) {
+            return getAvailabilityFromRestrictingAdmin();
+        }
         if (!Utils.isMultipleBiometricsSupported(mContext)) {
             return UNSUPPORTED_ON_DEVICE;
         }
+        return getAvailabilityFromRestrictingAdmin();
+    }
+
+    private int getAvailabilityFromRestrictingAdmin() {
         return getRestrictingAdmin() != null ? DISABLED_FOR_USER : AVAILABLE;
     }
 
diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java
index b8706a5..67c267d 100644
--- a/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/CombinedBiometricProfileStatusPreferenceController.java
@@ -62,4 +62,9 @@
     protected String getSettingsClassName() {
         return mCombinedBiometricStatusUtils.getProfileSettingsClassName();
     }
+
+    @Override
+    protected boolean isWorkProfileController() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java
index 50eb43d..a337c3b 100644
--- a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusPreferenceController.java
@@ -25,6 +25,7 @@
 import androidx.preference.PreferenceScreen;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.Utils;
 import com.android.settings.biometrics.BiometricStatusPreferenceController;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedPreference;
@@ -85,6 +86,11 @@
     }
 
     @Override
+    protected boolean isHardwareSupported() {
+        return Utils.hasFaceHardware(mContext) || Utils.hasFingerprintHardware(mContext);
+    }
+
+    @Override
     public void updateState(Preference preference) {
         super.updateState(preference);
         updateStateInternal();
diff --git a/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java
index a2e11af..1221389 100644
--- a/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java
@@ -84,4 +84,9 @@
                 mContext.getResources().getString(
                 R.string.security_settings_face_profile_preference_title)));
     }
+
+    @Override
+    protected boolean isWorkProfileController() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java
index f18a74f..c71119c 100644
--- a/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java
@@ -87,6 +87,11 @@
     }
 
     @Override
+    protected boolean isHardwareSupported() {
+        return Utils.hasFaceHardware(mContext);
+    }
+
+    @Override
     public void updateState(Preference preference) {
         super.updateState(preference);
         updateStateInternal();
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java
index d6d0b8f..051d254 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java
@@ -53,4 +53,9 @@
     protected int getUserId() {
         return mProfileChallengeUserId;
     }
+
+    @Override
+    protected boolean isWorkProfileController() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java
index 347fec7..fba93e1 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java
@@ -87,6 +87,11 @@
     }
 
     @Override
+    protected boolean isHardwareSupported() {
+        return Utils.hasFingerprintHardware(mContext);
+    }
+
+    @Override
     public void updateState(Preference preference) {
         super.updateState(preference);
         updateStateInternal();
diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java
new file mode 100644
index 0000000..a2907f8
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.activeunlock;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.ActiveUnlockTestUtils;
+import com.android.settings.testutils.shadow.ShadowDeviceConfig;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowDeviceConfig.class})
+public class ActiveUnlockStatusUtilsTest {
+
+    @Rule public final MockitoRule mMocks = MockitoJUnit.rule();
+
+    @Mock private PackageManager mPackageManager;
+    @Mock private FingerprintManager mFingerprintManager;
+    @Mock private FaceManager mFaceManager;
+
+    private Context mApplicationContext;
+    private ActiveUnlockStatusUtils mActiveUnlockStatusUtils;
+
+    @Before
+    public void setUp() {
+        mApplicationContext = spy(ApplicationProvider.getApplicationContext());
+        when(mApplicationContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        when(mApplicationContext.getSystemService(Context.FINGERPRINT_SERVICE))
+                .thenReturn(mFingerprintManager);
+        when(mApplicationContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+        ActiveUnlockTestUtils.enable(mApplicationContext);
+        mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(mApplicationContext);
+    }
+
+    @After
+    public void tearDown() {
+        ActiveUnlockTestUtils.disable(mApplicationContext);
+    }
+
+    @Test
+    public void isAvailable_featureFlagDisabled_returnsConditionallyUnavailable() {
+        ActiveUnlockTestUtils.disable(mApplicationContext);
+
+        assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
+    }
+
+    @Test
+    public void isAvailable_withoutFingerprint_withoutFace_returnsUnsupported() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void isAvailable_withoutFingerprint_withFace_returnsAvailable() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void isAvailable_withFingerprint_withoutFace_returnsAvailable() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void isAvailable_withFingerprint_withFace_returnsAvailable() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getAvailability()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void configIsUnlockOnIntent_useUnlockIntentLayoutIsTrue() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT);
+
+        assertThat(mActiveUnlockStatusUtils.useUnlockIntentLayout()).isTrue();
+        assertThat(mActiveUnlockStatusUtils.useBiometricFailureLayout()).isFalse();
+    }
+
+    @Test
+    public void configIsBiometricFailure_useBiometricFailureLayoutIsTrue() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT);
+
+        assertThat(mActiveUnlockStatusUtils.useUnlockIntentLayout()).isFalse();
+        assertThat(mActiveUnlockStatusUtils.useBiometricFailureLayout()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java
new file mode 100644
index 0000000..84a9ad4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFaceStatusPreferenceControllerTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.combination;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.UserManager;
+
+import com.android.settings.testutils.ActiveUnlockTestUtils;
+import com.android.settings.testutils.shadow.ShadowDeviceConfig;
+import com.android.settingslib.RestrictedPreference;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowDeviceConfig.class})
+public class BiometricFaceStatusPreferenceControllerTest {
+
+    @Rule public final MockitoRule mMocks = MockitoJUnit.rule();
+
+    @Mock private UserManager mUserManager;
+    @Mock private PackageManager mPackageManager;
+    @Mock private FingerprintManager mFingerprintManager;
+    @Mock private FaceManager mFaceManager;
+
+    private Context mContext;
+    private RestrictedPreference mPreference;
+    private BiometricFaceStatusPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        ShadowApplication.getInstance()
+                .setSystemService(Context.FINGERPRINT_SERVICE, mFingerprintManager);
+        ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager);
+        ShadowApplication.getInstance().setSystemService(Context.USER_SERVICE, mUserManager);
+        when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[] {1234});
+        mPreference = new RestrictedPreference(mContext);
+        mController = new BiometricFaceStatusPreferenceController(mContext, "preferenceKey");
+    }
+
+    @After
+    public void tearDown() {
+        ActiveUnlockTestUtils.disable(mContext);
+    }
+
+    @Test
+    public void onlyFaceEnabled_preferenceNotVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void onlyFaceAndActiveUnlockEnabled_preferenceVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+        ActiveUnlockTestUtils.enable(mContext);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void faceAndFingerprintEnabled_preferenceVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java
new file mode 100644
index 0000000..3eb4c21
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/combination/BiometricFingerprintStatusPreferenceControllerTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.combination;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.UserManager;
+
+import com.android.settings.testutils.ActiveUnlockTestUtils;
+import com.android.settings.testutils.shadow.ShadowDeviceConfig;
+import com.android.settingslib.RestrictedPreference;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowDeviceConfig.class})
+public class BiometricFingerprintStatusPreferenceControllerTest {
+
+    @Rule public final MockitoRule mMocks = MockitoJUnit.rule();
+
+    @Mock private UserManager mUserManager;
+    @Mock private PackageManager mPackageManager;
+    @Mock private FingerprintManager mFingerprintManager;
+    @Mock private FaceManager mFaceManager;
+
+    private Context mContext;
+    private RestrictedPreference mPreference;
+    private BiometricFingerprintStatusPreferenceController mController;
+
+    @Before
+    public void setUp() {
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        ShadowApplication.getInstance()
+                .setSystemService(Context.FINGERPRINT_SERVICE, mFingerprintManager);
+        ShadowApplication.getInstance().setSystemService(Context.FACE_SERVICE, mFaceManager);
+        ShadowApplication.getInstance().setSystemService(Context.USER_SERVICE, mUserManager);
+        when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[] {1234});
+        mPreference = new RestrictedPreference(mContext);
+        mController = new BiometricFingerprintStatusPreferenceController(mContext, "preferenceKey");
+    }
+
+    @After
+    public void tearDown() {
+        ActiveUnlockTestUtils.disable(mContext);
+    }
+
+    @Test
+    public void onlyFingerprintEnabled_preferenceNotVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void onlyFingerprintAndActiveUnlockEnabled_preferenceVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+        ActiveUnlockTestUtils.enable(mContext);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void faceAndFingerprintEnabled_preferenceVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        mController.updateState(mPreference);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java b/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java
new file mode 100644
index 0000000..0cecaee
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/ActiveUnlockTestUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.testutils;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+
+import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
+
+import java.util.ArrayList;
+
+/** Utilities class to enable or disable the Active Unlock flag in tests. */
+public final class ActiveUnlockTestUtils {
+
+    public static final String TARGET = "com.active.unlock.target";
+    public static final String PROVIDER = "com.active.unlock.provider";
+    public static final String TARGET_SETTING = "active_unlock_target";
+    public static final String PROVIDER_SETTING = "active_unlock_provider";
+
+    public static void enable(Context context) {
+        ActiveUnlockTestUtils.enable(context, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT);
+    }
+
+    public static void enable(Context context, String flagValue) {
+        Settings.Secure.putString(
+                context.getContentResolver(), TARGET_SETTING, TARGET);
+        Settings.Secure.putString(
+                context.getContentResolver(), PROVIDER_SETTING, PROVIDER);
+
+        PackageManager packageManager = context.getPackageManager();
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM;
+
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.activityInfo = new ActivityInfo();
+        resolveInfo.activityInfo.applicationInfo = applicationInfo;
+        when(packageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo);
+
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.applicationInfo = applicationInfo;
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = PROVIDER;
+        providerInfo.applicationInfo = applicationInfo;
+        packageInfo.providers = new ProviderInfo[] { providerInfo };
+        ArrayList<PackageInfo> packageInfos = new ArrayList<>();
+        packageInfos.add(packageInfo);
+        when(packageManager.getInstalledPackages(any())).thenReturn(packageInfos);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_REMOTE_AUTH,
+                ActiveUnlockStatusUtils.CONFIG_FLAG_NAME,
+                flagValue,
+                false /* makeDefault */);
+    }
+
+    public static void disable(Context context) {
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_REMOTE_AUTH,
+                ActiveUnlockStatusUtils.CONFIG_FLAG_NAME,
+                null /* value */,
+                false /* makeDefault */);
+    }
+}