Merge changes from topic "au-initial-commit-master"

* changes:
  Update text when Active Unlock is enabled.
  Update tile summary from ContentProvider.
  Add Active Unlock tile under face & fingerprint
  Add ActiveUnlock check when picking preference
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 540afb5..29a56e1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -6802,6 +6802,9 @@
     <!-- Search keyword for biometric settings. [CHAR_LIMIT=NONE]-->
     <string name="keywords_biometric_settings">face, fingerprint, add fingerprint</string>
 
+    <!-- Search keyword for active unlock settings. [CHAR_LIMIT=NONE]-->
+    <string name="keywords_active_unlock_settings">watch unlock, add watch unlock</string>
+
     <!-- Search keywords for adaptive brightness setting [CHAR LIMIT=NONE]-->
     <string name="keywords_display_auto_brightness">dim screen, touchscreen, battery, smart brightness, dynamic brightness, Auto brightness</string>
 
diff --git a/res/xml/security_settings_combined_biometric.xml b/res/xml/security_settings_combined_biometric.xml
index ef3a3fd..1bbe7b3 100644
--- a/res/xml/security_settings_combined_biometric.xml
+++ b/res/xml/security_settings_combined_biometric.xml
@@ -20,6 +20,7 @@
     android:title="@string/security_settings_biometric_preference_title">
 
     <com.android.settingslib.widget.TopIntroPreference
+        android:key="biometric_intro"
         android:title="@string/biometric_settings_intro" />
 
     <PreferenceCategory
@@ -40,6 +41,12 @@
             settings:keywords="@string/keywords_fingerprint_settings"
             settings:controller="com.android.settings.biometrics.combination.BiometricFingerprintStatusPreferenceController" />
 
+        <com.android.settingslib.RestrictedPreference
+            android:key="biometric_active_unlock_settings"
+            android:title="@string/security_settings_activeunlock_preference_title"
+            android:summary="@string/summary_placeholder"
+            settings:keywords="@string/keywords_active_unlock_settings"
+            settings:controller="com.android.settings.biometrics.activeunlock.ActiveUnlockStatusPreferenceController" />
     </PreferenceCategory>
 
     <PreferenceCategory
@@ -59,4 +66,4 @@
 
     </PreferenceCategory>
 
-</PreferenceScreen>
\ No newline at end of file
+</PreferenceScreen>
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/ActiveUnlockContentListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java
new file mode 100644
index 0000000..c2a8f39
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListener.java
@@ -0,0 +1,145 @@
+/*
+ * 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.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+/** Listens to updates from the content provider and fetches the latest value. */
+public class ActiveUnlockContentListener {
+
+    /** Callback interface for updates to values from the ContentProvider. */
+    public interface OnContentChangedListener {
+        /**
+         * Called when the content observer has updated.
+         *
+         * @param newValue the new value retrieved from the ContentProvider.
+         **/
+        void onContentChanged(@Nullable String newValue);
+    }
+
+    private static final String CONTENT_PROVIDER_PATH = "getSummary";
+
+    private final Context mContext;
+    private final OnContentChangedListener mContentChangedListener;
+    @Nullable private final Uri mUri;
+    private final String mLogTag;
+    private final String mMethodName;
+    private final String mContentKey;
+    @Nullable private String mContent;
+    private boolean mSubscribed = false;
+    private ContentObserver mContentObserver =
+            new ContentObserver(new Handler(Looper.getMainLooper())) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    getContentFromUri();
+                }
+            };
+
+    ActiveUnlockContentListener(
+            Context context,
+            OnContentChangedListener listener,
+            String logTag,
+            String methodName,
+            String contentKey) {
+        mContext = context;
+        mContentChangedListener = listener;
+        mLogTag = logTag;
+        mMethodName = methodName;
+        mContentKey = contentKey;
+        String authority = new ActiveUnlockStatusUtils(mContext).getAuthority();
+        if (authority != null) {
+            mUri = new Uri.Builder()
+                    .scheme(ContentResolver.SCHEME_CONTENT)
+                    .authority(authority)
+                    .appendPath(CONTENT_PROVIDER_PATH)
+                    .build();
+        } else {
+            mUri = null;
+        }
+
+    }
+
+    /** Starts listening for updates from the ContentProvider, and fetches the current value. */
+    public synchronized void subscribe() {
+        if (mSubscribed && mUri != null) {
+            return;
+        }
+        mSubscribed = true;
+        mContext.getContentResolver().registerContentObserver(
+                mUri, true /* notifyForDescendants */, mContentObserver);
+        ThreadUtils.postOnBackgroundThread(
+                () -> {
+                    getContentFromUri();
+                });
+    }
+
+    /** Stops listening for updates from the ContentProvider. */
+    public synchronized void unsubscribe() {
+        if (!mSubscribed && mUri != null) {
+            return;
+        }
+        mSubscribed = false;
+        mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+    }
+
+    /** Retrieves the most recently fetched value from the ContentProvider. */
+    @Nullable
+    public String getContent() {
+        return mContent;
+    }
+
+    private void getContentFromUri() {
+        if (mUri == null) {
+            Log.e(mLogTag, "Uri null when trying to fetch content");
+            return;
+        }
+        ContentResolver contentResolver = mContext.getContentResolver();
+        ContentProviderClient client = contentResolver.acquireContentProviderClient(mUri);
+        Bundle bundle;
+        try {
+            bundle = client.call(mMethodName, null /* arg */, null /* extras */);
+        } catch (RemoteException e) {
+            Log.e(mLogTag, "Failed to call contentProvider", e);
+            return;
+        } finally {
+            client.close();
+        }
+        if (bundle == null) {
+            Log.e(mLogTag, "Null bundle returned from contentProvider");
+            return;
+        }
+        String newValue = bundle.getString(mContentKey);
+        if (!TextUtils.equals(mContent, newValue)) {
+            mContent = newValue;
+            mContentChangedListener.onContentChanged(mContent);
+        }
+    }
+}
diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java
new file mode 100644
index 0000000..1badb0f
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockDeviceNameListener.java
@@ -0,0 +1,48 @@
+/*
+ * 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.Context;
+
+/** Listens to device name updates from the content provider and fetches the latest value. */
+public class ActiveUnlockDeviceNameListener  {
+    private static final String TAG = "ActiveUnlockDeviceNameListener";
+    private static final String METHOD_NAME = "getDeviceName";
+    private static final String DEVICE_NAME_KEY = "com.android.settings.active_unlock.device_name";
+
+    private final ActiveUnlockContentListener mActiveUnlockContentListener;
+    public ActiveUnlockDeviceNameListener(
+            Context context, ActiveUnlockContentListener.OnContentChangedListener listener) {
+        mActiveUnlockContentListener = new ActiveUnlockContentListener(
+            context, listener, TAG, METHOD_NAME, DEVICE_NAME_KEY);
+    }
+
+    /** Returns whether a device is enrolled in Active Unlock. */
+    public boolean hasEnrolled() {
+        return mActiveUnlockContentListener.getContent() != null;
+    }
+
+    /** Subscribes to device name updates. */
+    public void subscribe() {
+        mActiveUnlockContentListener.subscribe();
+    }
+
+    /** Unsubscribes from device name updates. */
+    public void unsubscribe() {
+        mActiveUnlockContentListener.unsubscribe();
+    }
+}
diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java
new file mode 100644
index 0000000..05d4acb
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceController.java
@@ -0,0 +1,135 @@
+/*
+ * 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.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.Utils;
+import com.android.settings.biometrics.BiometricStatusPreferenceController;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener;
+import com.android.settingslib.RestrictedPreference;
+
+/**
+ * Preference controller for active unlock settings within the biometrics settings page, that
+ * controls the ability to unlock the phone with watch authentication.
+ */
+public class ActiveUnlockStatusPreferenceController
+        extends BiometricStatusPreferenceController
+        implements LifecycleObserver, OnContentChangedListener {
+    /**
+     * Preference key.
+     *
+     * This must match the key found in security_settings_combined_biometric.xml
+     **/
+    public static final String KEY_ACTIVE_UNLOCK_SETTINGS = "biometric_active_unlock_settings";
+    @Nullable private RestrictedPreference mPreference;
+    @Nullable private PreferenceScreen mPreferenceScreen;
+    @Nullable private String mSummary;
+    private final ActiveUnlockStatusUtils mActiveUnlockStatusUtils;
+    private final ActiveUnlockSummaryListener mActiveUnlockSummaryListener;
+
+    public ActiveUnlockStatusPreferenceController(@NonNull Context context) {
+        this(context, KEY_ACTIVE_UNLOCK_SETTINGS);
+    }
+
+    public ActiveUnlockStatusPreferenceController(
+            @NonNull Context context, @NonNull String key) {
+        super(context, key);
+        mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(context);
+        mActiveUnlockSummaryListener = new ActiveUnlockSummaryListener(context, this);
+    }
+
+
+    /** Subscribes to update preference summary dynamically. */
+    @OnLifecycleEvent(Lifecycle.Event.ON_START)
+    public void onStart() {
+        mActiveUnlockSummaryListener.subscribe();
+    }
+
+    /** Resets the preference reference on resume. */
+    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+    public void onResume() {
+        if (mPreferenceScreen != null) {
+            displayPreference(mPreferenceScreen);
+        }
+    }
+
+    /** Unsubscribes to prevent leaked listener. */
+    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+    public void onStop() {
+        mActiveUnlockSummaryListener.unsubscribe();
+    }
+
+    @Override
+    public void onContentChanged(String newContent) {
+        mSummary = newContent;
+        if (mPreference != null) {
+            mPreference.setSummary(getSummaryText());
+        }
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreferenceScreen = screen;
+        mPreference = screen.findPreference(mPreferenceKey);
+        updateState(mPreference);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return mActiveUnlockStatusUtils.getAvailability();
+    }
+
+    @Override
+    protected boolean isDeviceSupported() {
+        // This should never be called, as getAvailabilityStatus() will return the exact value.
+        // However, this is an abstract method in BiometricStatusPreferenceController, and so
+        // needs to be overridden.
+        return mActiveUnlockStatusUtils.isAvailable();
+    }
+
+    @Override
+    protected boolean isHardwareSupported() {
+        // This should never be called, as getAvailabilityStatus() will return the exact value.
+        // However, this is an abstract method in BiometricStatusPreferenceController, and so
+        // needs to be overridden.
+        return Utils.hasFaceHardware(mContext) || Utils.hasFingerprintHardware(mContext);
+    }
+
+    @Override
+    protected String getSummaryText() {
+        if (mSummary == null) {
+            // return non-empty string to prevent re-sizing of the tile
+            return " ";
+        }
+        return mSummary;
+    }
+
+    @Override
+    protected String getSettingsClassName() {
+        // TODO(b/264813445): direct user to face & fingerprint setup
+        return null;
+    }
+}
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..439f176
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtils.java
@@ -0,0 +1,248 @@
+/*
+ * 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 androidx.annotation.StringRes;
+
+import com.android.settings.R;
+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;
+    }
+
+    /**
+     * Returns the title of the combined biometric settings entity when active unlock is enabled.
+     */
+    public String getTitleForActiveUnlock() {
+        final boolean faceAllowed = Utils.hasFaceHardware(mContext);
+        final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext);
+        return mContext.getString(getTitleRes(faceAllowed, fingerprintAllowed));
+    }
+
+    @StringRes
+    private static int getTitleRes(boolean isFaceAllowed, boolean isFingerprintAllowed) {
+        if (isFaceAllowed && isFingerprintAllowed) {
+            return R.string.security_settings_biometric_preference_title;
+        } else if (isFaceAllowed) {
+            return R.string.security_settings_face_preference_title;
+        } else if (isFingerprintAllowed) {
+            return R.string.security_settings_fingerprint_preference_title;
+        } else {
+            // Default to original summary, but this case should never happen.
+            return R.string.security_settings_biometric_preference_title;
+        }
+    }
+
+    /**
+     * Returns the intro of the combined biometric settings entity when active unlock is enabled.
+     */
+    public String getIntroForActiveUnlock() {
+        final boolean faceAllowed = Utils.hasFaceHardware(mContext);
+        final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext);
+        if (useBiometricFailureLayout()) {
+            int introRes = getIntroRes(faceAllowed, fingerprintAllowed);
+            return introRes == 0 ? "" : mContext.getString(introRes);
+        }
+        if (useUnlockIntentLayout() && (!faceAllowed || !fingerprintAllowed)) {
+            return "";
+        }
+        return mContext.getString(R.string.biometric_settings_intro);
+    }
+
+    @StringRes
+    private static int getIntroRes(boolean isFaceAllowed, boolean isFingerprintAllowed) {
+        if (isFaceAllowed && isFingerprintAllowed) {
+            return R.string.biometric_settings_intro_with_activeunlock;
+        } else if (isFaceAllowed) {
+            return R.string.biometric_settings_intro_with_face;
+        } else if (isFingerprintAllowed) {
+            return R.string.biometric_settings_intro_with_fingerprint;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Returns the summary of the unlock device entity when active unlock is enabled.
+     */
+    public String getUnlockDeviceSummaryForActiveUnlock() {
+        final boolean faceAllowed = Utils.hasFaceHardware(mContext);
+        final boolean fingerprintAllowed = Utils.hasFingerprintHardware(mContext);
+
+        return mContext.getString(getUnlockDeviceSummaryRes(faceAllowed, fingerprintAllowed));
+    }
+
+    @StringRes
+    private static int getUnlockDeviceSummaryRes(
+            boolean isFaceAllowed, boolean isFingerprintAllowed) {
+        if (isFaceAllowed && isFingerprintAllowed) {
+            return R.string.biometric_settings_use_face_fingerprint_or_watch_preference_summary;
+        } else if (isFaceAllowed) {
+            return R.string.biometric_settings_use_face_or_watch_preference_summary;
+        } else if (isFingerprintAllowed) {
+            return R.string.biometric_settings_use_fingerprint_or_watch_preference_summary;
+        } else {
+            return R.string.biometric_settings_use_watch_preference_summary;
+        }
+    }
+
+    private static String getFlagState() {
+        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_REMOTE_AUTH, CONFIG_FLAG_NAME);
+    }
+}
diff --git a/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java
new file mode 100644
index 0000000..bcffe62
--- /dev/null
+++ b/src/com/android/settings/biometrics/activeunlock/ActiveUnlockSummaryListener.java
@@ -0,0 +1,43 @@
+/*
+ * 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.Context;
+
+/** Listens to summary updates from the content provider and fetches the latest value. */
+public class ActiveUnlockSummaryListener {
+    private static final String TAG = "ActiveUnlockSummaryListener";
+    private static final String METHOD_NAME = "getSummary";
+    private static final String SUMMARY_KEY = "com.android.settings.summary";
+
+    private final ActiveUnlockContentListener mContentListener;
+    public ActiveUnlockSummaryListener(
+            Context context, ActiveUnlockContentListener.OnContentChangedListener listener) {
+        mContentListener = new ActiveUnlockContentListener(
+                context, listener, TAG, METHOD_NAME, SUMMARY_KEY);
+    }
+
+    /** Subscribes for summary updates. */
+    public void subscribe() {
+        mContentListener.subscribe();
+    }
+
+    /** Unsubscribes from summary updates. */
+    public void unsubscribe() {
+        mContentListener.unsubscribe();
+    }
+}
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/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
index 4da42d4..0a1d29d 100644
--- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
+++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
@@ -107,10 +107,7 @@
             launchChooseOrConfirmLock();
         }
 
-        final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey());
-        if (unlockPhonePreference != null) {
-            unlockPhonePreference.setSummary(getUseAnyBiometricSummary());
-        }
+        updateUnlockPhonePreferenceSummary();
 
         final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey());
         if (useInAppsPreference != null) {
@@ -309,8 +306,15 @@
         }
     }
 
+    protected void updateUnlockPhonePreferenceSummary() {
+        final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey());
+        if (unlockPhonePreference != null) {
+            unlockPhonePreference.setSummary(getUseAnyBiometricSummary());
+        }
+    }
+
     @NonNull
-    private String getUseAnyBiometricSummary() {
+    protected String getUseAnyBiometricSummary() {
         boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected();
         boolean isFingerprintAllowed =
                 mFingerprintManager != null && mFingerprintManager.isHardwareDetected();
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/CombinedBiometricSettings.java b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java
index 7e76ceb..a352e5a 100644
--- a/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java
+++ b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java
@@ -17,8 +17,15 @@
 
 import android.app.settings.SettingsEnums;
 import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockDeviceNameListener;
+import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settingslib.search.SearchIndexable;
 
@@ -32,6 +39,10 @@
     private static final String KEY_FINGERPRINT_SETTINGS = "biometric_fingerprint_settings";
     private static final String KEY_UNLOCK_PHONE = "biometric_settings_biometric_keyguard";
     private static final String KEY_USE_IN_APPS = "biometric_settings_biometric_app";
+    private static final String KEY_INTRO_PREFERENCE = "biometric_intro";
+
+    private ActiveUnlockStatusUtils mActiveUnlockStatusUtils;
+    @Nullable private ActiveUnlockDeviceNameListener mActiveUnlockDeviceNameListener;
 
     @Override
     public void onAttach(Context context) {
@@ -41,6 +52,41 @@
     }
 
     @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mActiveUnlockStatusUtils = new ActiveUnlockStatusUtils(getActivity());
+        if (mActiveUnlockStatusUtils.isAvailable()) {
+            updateUiForActiveUnlock();
+        }
+    }
+
+    private void updateUiForActiveUnlock() {
+        OnContentChangedListener listener = new OnContentChangedListener() {
+            @Override
+            public void onContentChanged(String newValue) {
+                updateUnlockPhonePreferenceSummary();
+            }
+        };
+
+        mActiveUnlockDeviceNameListener =
+                new ActiveUnlockDeviceNameListener(getActivity(), listener);
+        mActiveUnlockDeviceNameListener.subscribe();
+        final Preference introPreference = findPreference(KEY_INTRO_PREFERENCE);
+        if (introPreference != null) {
+            introPreference.setTitle(mActiveUnlockStatusUtils.getIntroForActiveUnlock());
+        }
+        getActivity().setTitle(mActiveUnlockStatusUtils.getTitleForActiveUnlock());
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mActiveUnlockDeviceNameListener != null) {
+            mActiveUnlockDeviceNameListener.unsubscribe();
+        }
+        super.onDestroy();
+    }
+
+    @Override
     protected int getPreferenceScreenResId() {
         return R.xml.security_settings_combined_biometric;
     }
@@ -75,6 +121,16 @@
         return SettingsEnums.COMBINED_BIOMETRIC;
     }
 
+    @Override
+    protected String getUseAnyBiometricSummary() {
+        // either Active Unlock is not enabled or no device is enrolled.
+        if (mActiveUnlockDeviceNameListener == null
+                || !mActiveUnlockDeviceNameListener.hasEnrolled()) {
+            return super.getUseAnyBiometricSummary();
+        }
+        return mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock();
+    }
+
     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
             new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric);
 }
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/ActiveUnlockContentListenerTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListenerTest.java
new file mode 100644
index 0000000..cb0c942
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockContentListenerTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.idleMainLooper;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics.activeunlock.ActiveUnlockContentListener.OnContentChangedListener;
+import com.android.settings.testutils.ActiveUnlockTestUtils;
+import com.android.settings.testutils.shadow.ShadowDeviceConfig;
+
+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.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowDeviceConfig.class})
+public class ActiveUnlockContentListenerTest {
+
+    @Rule public final MockitoRule mMocks = MockitoJUnit.rule();
+    @Mock private PackageManager mPackageManager;
+
+    private Context mContext;
+    private ActiveUnlockContentListener mContentListener;
+    @Nullable private String mContent;
+    private int mUpdateCount;
+
+    @Before
+    public void setUp() {
+        Robolectric.setupContentProvider(
+                FakeContentProvider.class, FakeContentProvider.AUTHORITY);
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        OnContentChangedListener listener = new OnContentChangedListener() {
+            @Override
+            public void onContentChanged(String newValue) {
+                mContent = newValue;
+                mUpdateCount++;
+            }
+        };
+        ActiveUnlockTestUtils.enable(mContext);
+        mContentListener =
+                new ActiveUnlockContentListener(
+                        mContext,
+                        listener,
+                        "logTag",
+                        FakeContentProvider.METHOD_SUMMARY,
+                        FakeContentProvider.KEY_SUMMARY);
+        FakeContentProvider.init(mContext);
+    }
+
+    @Test
+    public void subscribe_contentFetched() {
+        String newContent = "newContent";
+        FakeContentProvider.setTileSummary(newContent);
+
+        mContentListener.subscribe();
+        idleMainLooper();
+
+        assertThat(mContent).isEqualTo(newContent);
+    }
+
+    @Test
+    public void contentUpdated_contentUpdated() {
+        mContentListener.subscribe();
+        idleMainLooper();
+
+        String newContent = "newContent";
+        updateContent(newContent);
+
+        assertThat(mContent).isEqualTo(newContent);
+    }
+
+    @Test
+    public void contentUpdated_unsubscribed_contentNotUpdated() {
+        mContentListener.subscribe();
+        idleMainLooper();
+
+        mContentListener.unsubscribe();
+        updateContent("newContent");
+
+        assertThat(mContent).isNull();
+    }
+
+    @Test
+    public void multipleContentUpdates_contentIsNewestValueAndUpdatedTwice() {
+        mContentListener.subscribe();
+        idleMainLooper();
+
+        updateContent("temporaryContent");
+        String newContent = "newContent";
+        updateContent(newContent);
+
+        assertThat(mContent).isEqualTo(newContent);
+        assertThat(mUpdateCount).isEqualTo(2);
+    }
+
+    @Test
+    public void duplicateContentUpdates_onContentChangedOnlyCalledOnce() {
+        mContentListener.subscribe();
+        idleMainLooper();
+
+        updateContent("newContent");
+        updateContent("newContent");
+
+        assertThat(mUpdateCount).isEqualTo(1);
+    }
+
+    private void updateContent(String content) {
+        FakeContentProvider.setTileSummary(content);
+        mContext.getContentResolver().notifyChange(
+                FakeContentProvider.URI, null /* observer */);
+        idleMainLooper();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.java
new file mode 100644
index 0000000..bf60d01
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusPreferenceControllerTest.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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.idleMainLooper;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.UserManager;
+
+import androidx.preference.PreferenceScreen;
+
+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.Robolectric;
+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 ActiveUnlockStatusPreferenceControllerTest {
+
+    @Rule public final MockitoRule mMocks = MockitoJUnit.rule();
+
+    @Mock private UserManager mUserManager;
+    @Mock private PackageManager mPackageManager;
+    @Mock private FingerprintManager mFingerprintManager;
+    @Mock private FaceManager mFaceManager;
+    @Mock private PreferenceScreen mPreferenceScreen;
+
+    private Context mContext;
+    private ActiveUnlockStatusPreferenceController mController;
+    private RestrictedPreference mPreference;
+
+    @Before
+    public void setUp() {
+        Robolectric.setupContentProvider(FakeContentProvider.class, FakeContentProvider.AUTHORITY);
+        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);
+        when(mPreferenceScreen.findPreference(any())).thenReturn(mPreference);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+        ActiveUnlockTestUtils.enable(mContext);
+        FakeContentProvider.init(mContext);
+        mController = new ActiveUnlockStatusPreferenceController(mContext);
+    }
+
+    @After
+    public void tearDown() {
+        ActiveUnlockTestUtils.disable(mContext);
+    }
+
+    @Test
+    public void updateState_featureFlagDisabled_isNotVisible() {
+        ActiveUnlockTestUtils.disable(mContext);
+
+        mController.displayPreference(mPreferenceScreen);
+
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateState_withoutFingerprint_withoutFace_isNotVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        mController.displayPreference(mPreferenceScreen);
+
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateState_withoutFingerprint_withFace_isVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        mController.displayPreference(mPreferenceScreen);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void updateState_withFingerprint_withoutFace_isVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        mController.displayPreference(mPreferenceScreen);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void updateState_withFingerprint_withFace_isVisible() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        mController.displayPreference(mPreferenceScreen);
+
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void defaultState_summaryIsEmpty() {
+        mController.displayPreference(mPreferenceScreen);
+
+        idleMainLooper();
+
+        assertThat(mPreference.getSummary().toString()).isEqualTo(" ");
+    }
+
+    @Test
+    public void onStart_summaryIsUpdated() {
+        String summary = "newSummary";
+        updateSummary(summary);
+        mController.displayPreference(mPreferenceScreen);
+
+        mController.onStart();
+        idleMainLooper();
+
+        assertThat(mPreference.getSummary().toString()).isEqualTo(summary);
+    }
+
+    private void updateSummary(String summary) {
+        FakeContentProvider.setTileSummary(summary);
+        mContext.getContentResolver().notifyChange(FakeContentProvider.URI, null /* observer */);
+        idleMainLooper();
+    }
+}
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..d420f78
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/ActiveUnlockStatusUtilsTest.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.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.R;
+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();
+    }
+
+    @Test
+    public void getTitle_faceEnabled_returnsFacePreferenceTitle() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.security_settings_face_preference_title));
+    }
+
+    @Test
+    public void getTitle_fingerprintEnabled_returnsFingerprintPreferenceTitle() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getTitleForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.security_settings_fingerprint_preference_title));
+    }
+
+    @Test
+    public void getIntro_faceEnabled_returnsIntroWithFace() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.biometric_settings_intro_with_face));
+    }
+
+    @Test
+    public void getIntro_fingerprintEnabled_returnsIntroWithFingerprint() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.BIOMETRIC_FAILURE_LAYOUT);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.biometric_settings_intro_with_fingerprint));
+    }
+
+    @Test
+    public void getIntro_unlockOnIntentAndFaceEnabled_returnsEmpty() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock()).isEqualTo("");
+    }
+
+    @Test
+    public void getIntro_unlockOnIntentAndFaceAndFingerprintEnabled_returnsDefault() {
+        ActiveUnlockTestUtils.enable(
+                mApplicationContext, ActiveUnlockStatusUtils.UNLOCK_INTENT_LAYOUT);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getIntroForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.biometric_settings_intro));
+    }
+
+    @Test
+    public void getUnlockDeviceSummary_fingerprintEnabled_returnsFingerprintOrWatch() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+
+        assertThat(mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.biometric_settings_use_fingerprint_or_watch_preference_summary));
+    }
+
+    @Test
+    public void getUnlockDeviceSummary_faceEnabled_returnsFaceOrWatch() {
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+
+        assertThat(mActiveUnlockStatusUtils.getUnlockDeviceSummaryForActiveUnlock())
+                .isEqualTo(mApplicationContext.getString(
+                        R.string.biometric_settings_use_face_or_watch_preference_summary));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java b/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java
new file mode 100644
index 0000000..7bb6941
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/activeunlock/FakeContentProvider.java
@@ -0,0 +1,108 @@
+/*
+ * 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.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.testutils.ActiveUnlockTestUtils;
+
+/** ContentProvider to provider tile summary for ActiveUnlock in tests. */
+public final class FakeContentProvider extends ContentProvider {
+    public static final String AUTHORITY = ActiveUnlockTestUtils.PROVIDER;
+    public static final Uri URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(AUTHORITY)
+            .appendPath("getSummary")
+            .build();
+    public static final String METHOD_SUMMARY = "getSummary";
+    public static final String KEY_SUMMARY = "com.android.settings.summary";
+    private static final String METHOD_DEVICE_NAME = "getDeviceName";
+    private static final String KEY_DEVICE_NAME = "com.android.settings.active_unlock.device_name";
+    @Nullable private static String sTileSummary;
+    @Nullable private static String sDeviceName;
+
+    public FakeContentProvider() {
+        super();
+    }
+
+    public static void setTileSummary(String summary) {
+        sTileSummary = summary;
+    }
+
+    public static void setDeviceName(String deviceName) {
+        sDeviceName = deviceName;
+    }
+
+    public static void init(Context context) {
+        Settings.Secure.putString(
+                context.getContentResolver(), ActiveUnlockTestUtils.PROVIDER_SETTING, AUTHORITY);
+        sTileSummary = null;
+        sDeviceName = null;
+    }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        Bundle bundle = new Bundle();
+        if (METHOD_SUMMARY.equals(method)) {
+            bundle.putCharSequence(KEY_SUMMARY, sTileSummary);
+        } else if (METHOD_DEVICE_NAME.equals(method)) {
+            bundle.putCharSequence(KEY_DEVICE_NAME, sDeviceName);
+        }
+        return bundle;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+}
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 */);
+    }
+}