Add Private Space settings page

This involves:
1. Adding a new page for PS settings under Security & Privacy
2. Integrating the PS safety source with SafetyCenter.

Also, add the capability to create and delete PS from the page.
Creation is temporary to help with prototyping, and will be removed
finally.

Screenshots:
Private Space Entry point in Security & Privacy Settings:
https://screenshot.googleplex.com/4VHxNekjhxZHJwF

Private Space Settings page:
https://screenshot.googleplex.com/3Raw4wuymTHTgtM

Creating Private Space:
https://screenshot.googleplex.com/3dvzAH6V4kQmuYf

Private Space created:
https://screenshot.googleplex.com/Aj7nnF9uuUCa9Q5

Design doc:
https://docs.google.com/document/d/1CdjUjAE84-5ZEPRIfw5KYFjLVHtEZxc_sF0w95su8DA/edit?usp=sharing&resourcekey=0-dAACT9HRexY1IyoxMmkVlw

Bug: 286539356
Bug: 293569406
Test: manual
Test: atest DeletePrivateSpaceControllerTest
Test: atest UseOneLockControllerTest
Test: atest HidePrivateSpaceControllerTest
Change-Id: I9caf8e04e7fb2df36e60f607225e2931988ee692
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d106f4..9942534 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1175,13 +1175,38 @@
 
     <!-- Title for the subpage in the "Security & privacy". This page consists of the more security and privacy settings. Can be navigated by Settings -> Security & privacy -> More security & privacy [CHAR LIMIT=NONE] -->
     <string name="more_security_privacy_settings">More security &amp; privacy</string>
-    <!-- Title for the section that has security entries in the More security & privacy page. [CHAR LIMIT=60] -->
+    <!-- Title for the section that has security entries in the More security & privacy page. Also used for Private Space security category. [CHAR LIMIT=60] -->
     <string name="security_header">Security</string>
     <!-- Title for the section that has privacy entries in the More security & privacy page. [CHAR LIMIT=60] -->
     <string name="privacy_header">Privacy</string>
     <!-- Title for the section that has work profile entries in the More security & privacy page. [CHAR LIMIT=60] -->
     <string name="work_profile_category_header">Work profile</string>
 
+    <!-- Title for the Private Space page. [CHAR LIMIT=60] -->
+    <string name="private_space_title">Private Space</string>
+    <!-- Summary for the Private Space page. [CHAR LIMIT=NONE] -->
+    <string name="private_space_summary">Hide apps in a private folder</string>
+    <!-- Title for the Private Space one lock preference. [CHAR LIMIT=60] -->
+    <string name="private_space_one_lock_title">Unlock using screen lock</string>
+    <!-- Title for the preference to hide Private Space. [CHAR LIMIT=60] -->
+    <string name="private_space_hide_title">Show Private Space</string>
+    <!-- System category for the Private Space page. [CHAR LIMIT=30] -->
+    <string name="private_space_category_system">System</string>
+    <!-- Title for the preference to create Private Space. [CHAR LIMIT=60] -->
+    <string name="private_space_create_title">Create Private Space</string>
+    <!-- Title for the preference to delete Private Space. [CHAR LIMIT=60] -->
+    <string name="private_space_delete_title">Delete Private Space</string>
+    <!-- Toast to show when the private space was created. [CHAR LIMIT=NONE] -->
+    <string name="private_space_created">Private Space successfully created</string>
+    <!-- Toast to show when the private space already exists. [CHAR LIMIT=NONE] -->
+    <string name="private_space_already_exists">Private Space already exists</string>
+    <!-- Toast to show when the private space could not be created. [CHAR LIMIT=NONE] -->
+    <string name="private_space_create_failed">Private Space could not be created</string>
+    <!-- Toast to show when the private space was deleted. [CHAR LIMIT=NONE] -->
+    <string name="private_space_deleted">Private Space successfully deleted</string>
+    <!-- Toast to show when the private space could not be deleted. [CHAR LIMIT=NONE] -->
+    <string name="private_space_delete_failed">Private Space could not be deleted</string>
+
     <!-- Text shown when "Add fingerprint" button is disabled -->
     <string name="fingerprint_add_max">You can add up to <xliff:g id="count" example="5">%d</xliff:g> fingerprints</string>
     <!-- Text shown when users has enrolled a maximum number of fingerprints [CHAR LIMIT=NONE] -->
diff --git a/res/xml/private_space_settings.xml b/res/xml/private_space_settings.xml
new file mode 100644
index 0000000..08053e0
--- /dev/null
+++ b/res/xml/private_space_settings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:key="private_space_title"
+    android:title="@string/private_space_title">
+
+    <PreferenceCategory
+        android:title="@string/security_header">
+
+        <SwitchPreference
+            android:key="private_space_use_one_lock"
+            android:title="@string/private_space_one_lock_title"
+            settings:controller="com.android.settings.privatespace.UseOneLockController"
+            settings:searchable="false" />
+
+        <SwitchPreference
+            android:key="private_space_hidden"
+            android:title="@string/private_space_hide_title"
+            settings:controller="com.android.settings.privatespace.HidePrivateSpaceController"
+            settings:searchable="false" />
+
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:title="@string/private_space_category_system">
+
+        <Preference
+            android:key="private_space_create"
+            android:title="@string/private_space_create_title"
+            settings:controller="com.android.settings.privatespace.CreatePrivateSpaceController"
+            settings:searchable="false" />
+
+        <Preference
+            android:key="private_space_delete"
+            android:title="@string/private_space_delete_title"
+            settings:controller="com.android.settings.privatespace.DeletePrivateSpaceController"
+            settings:searchable="false" />
+
+    </PreferenceCategory>
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/settings/privatespace/CreatePrivateSpaceController.java b/src/com/android/settings/privatespace/CreatePrivateSpaceController.java
new file mode 100644
index 0000000..3214988
--- /dev/null
+++ b/src/com/android/settings/privatespace/CreatePrivateSpaceController.java
@@ -0,0 +1,74 @@
+/*
+ * 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.privatespace;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+// TODO(b/293569406): Remove this when we have the setup flow in place to create PS
+/**
+ * Temp Controller to create the private space from the PS Settings page. This is to allow PM, UX,
+ * and other folks to play around with PS before the PS setup flow is ready.
+ */
+public final class CreatePrivateSpaceController extends BasePreferenceController {
+
+    public CreatePrivateSpaceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
+            return false;
+        }
+
+        if (PrivateSpaceMaintainer.getInstance(mContext).doesPrivateSpaceExist()) {
+            showPrivateSpaceAlreadyExistsToast();
+            return super.handlePreferenceTreeClick(preference);
+        }
+
+        if (PrivateSpaceMaintainer.getInstance(mContext).createPrivateSpace()) {
+            showPrivateSpaceCreatedToast();
+        } else {
+            showPrivateSpaceCreationFailedToast();
+        }
+        return super.handlePreferenceTreeClick(preference);
+    }
+
+    private void showPrivateSpaceCreatedToast() {
+        Toast.makeText(mContext, R.string.private_space_created, Toast.LENGTH_SHORT).show();
+    }
+
+    private void showPrivateSpaceCreationFailedToast() {
+        Toast.makeText(mContext, R.string.private_space_create_failed, Toast.LENGTH_SHORT).show();
+    }
+
+    private void showPrivateSpaceAlreadyExistsToast() {
+        Toast.makeText(mContext, R.string.private_space_already_exists, Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/src/com/android/settings/privatespace/DeletePrivateSpaceController.java b/src/com/android/settings/privatespace/DeletePrivateSpaceController.java
new file mode 100644
index 0000000..c94f63a
--- /dev/null
+++ b/src/com/android/settings/privatespace/DeletePrivateSpaceController.java
@@ -0,0 +1,90 @@
+/*
+ * 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.privatespace;
+
+import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
+import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
+import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.preference.Preference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+/** Controller to delete the private space from the PS Settings page */
+public class DeletePrivateSpaceController extends BasePreferenceController {
+    private static final String TAG = "DeletePrivateSpaceController";
+    private final PrivateSpaceMaintainer mPrivateSpaceMaintainer;
+
+    static class Injector {
+        PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) {
+            return PrivateSpaceMaintainer.getInstance(context);
+        }
+    }
+
+    public DeletePrivateSpaceController(Context context, String preferenceKey) {
+        this(context, preferenceKey, new Injector());
+    }
+
+    DeletePrivateSpaceController(Context context, String preferenceKey, Injector injector) {
+        super(context, preferenceKey);
+        mPrivateSpaceMaintainer = injector.injectPrivateSpaceMaintainer(context);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) {
+            return false;
+        }
+
+        PrivateSpaceMaintainer.ErrorDeletingPrivateSpace error =
+                mPrivateSpaceMaintainer.deletePrivateSpace();
+        if (error == DELETE_PS_ERROR_NONE) {
+            showSuccessfulDeletionToast();
+        } else if (error == DELETE_PS_ERROR_INTERNAL) {
+            showDeletionInternalErrorToast();
+        } else if (error == DELETE_PS_ERROR_NO_PRIVATE_SPACE) {
+            // Ideally this should never happen as PS Settings is not available when there's no
+            // Private Profile.
+            Log.e(TAG, "Unexpected attempt to delete non-existent PS");
+        }
+        return super.handlePreferenceTreeClick(preference);
+    }
+
+    /** Shows a toast saying that the private space was deleted */
+    @VisibleForTesting
+    public void showSuccessfulDeletionToast() {
+        Toast.makeText(mContext, R.string.private_space_deleted, Toast.LENGTH_SHORT).show();
+    }
+
+    /** Shows a toast saying that the private space could not be deleted */
+    @VisibleForTesting
+    public void showDeletionInternalErrorToast() {
+        Toast.makeText(mContext, R.string.private_space_delete_failed, Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/src/com/android/settings/privatespace/HidePrivateSpaceController.java b/src/com/android/settings/privatespace/HidePrivateSpaceController.java
new file mode 100644
index 0000000..f27acbd
--- /dev/null
+++ b/src/com/android/settings/privatespace/HidePrivateSpaceController.java
@@ -0,0 +1,50 @@
+/*
+ * 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.privatespace;
+
+import android.content.Context;
+
+import com.android.settings.core.TogglePreferenceController;
+
+/** Represents the preference controller for (un)hiding the Private Space */
+public final class HidePrivateSpaceController extends TogglePreferenceController {
+    public HidePrivateSpaceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public boolean isChecked() {
+        // TODO(b/293569406) Need to check this from a persistent store, maybe like SettingsProvider
+        return false;
+    }
+
+    @Override
+    public boolean setChecked(boolean isChecked) {
+        // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider
+        return true;
+    }
+
+    @Override
+    public int getSliceHighlightMenuRes() {
+        return 0;
+    }
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java b/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java
new file mode 100644
index 0000000..9e1d0d5
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceDashboardFragment.java
@@ -0,0 +1,78 @@
+/*
+ * 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.privatespace;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.util.FeatureFlagUtils;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
+import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.search.SearchIndexable;
+
+import java.util.List;
+
+/** Fragment representing the Private Space dashboard in Settings. */
+@SearchIndexable
+public class PrivateSpaceDashboardFragment extends DashboardFragment {
+    private static final String TAG = "PrivateSpaceDashboardFragment";
+    private static final String KEY_CREATE_PROFILE_PREFERENCE = "private_space_create";
+    private static final String KEY_DELETE_PROFILE_PREFERENCE = "private_space_delete";
+    private static final String KEY_ONE_LOCK_PREFERENCE = "private_space_use_one_lock";
+    private static final String KEY_PS_HIDDEN_PREFERENCE = "private_space_hidden";
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.private_space_settings;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.PRIVATE_SPACE_SETTINGS;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+            new BaseSearchIndexProvider(R.xml.private_space_settings) {
+                @Override
+                protected boolean isPageSearchEnabled(Context context) {
+                    // Temporary workaround for hiding PS Settings until the trunk stable feature
+                    // flag is available.
+                    // TODO(b/295516544): Remove this workaround when trunk stable feature flag is
+                    // available.
+                    return SafetyCenterManagerWrapper.get().isEnabled(context)
+                            && FeatureFlagUtils.isEnabled(context,
+                            FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS);
+                }
+
+                @Override
+                public List<String> getNonIndexableKeys(Context context) {
+                    List<String> keys = super.getNonIndexableKeys(context);
+                    keys.add(KEY_CREATE_PROFILE_PREFERENCE);
+                    keys.add(KEY_DELETE_PROFILE_PREFERENCE);
+                    keys.add(KEY_ONE_LOCK_PREFERENCE);
+                    keys.add(KEY_PS_HIDDEN_PREFERENCE);
+                    return keys;
+                }
+            };
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
new file mode 100644
index 0000000..709814d
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.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.privatespace;
+
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.List;
+
+// TODO(b/293569406): Update the javadoc when we have the setup flow in place to create PS
+/** A class to help with the creation / deletion of Private Space */
+public class PrivateSpaceMaintainer {
+    private static final String TAG = "PrivateSpaceMaintainer";
+    @GuardedBy("this")
+    private static PrivateSpaceMaintainer sPrivateSpaceMaintainer;
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+    @GuardedBy("this")
+    private UserHandle mUserHandle;
+
+    public enum ErrorDeletingPrivateSpace {
+            DELETE_PS_ERROR_NONE,
+            DELETE_PS_ERROR_NO_PRIVATE_SPACE,
+            DELETE_PS_ERROR_INTERNAL
+    }
+
+    /**
+     * Returns true if the private space was successfully created.
+     *
+     * <p> This method should be used by the Private Space Setup Flow ONLY.
+     */
+    final synchronized boolean createPrivateSpace() {
+        // Check if Private space already exists
+        if (doesPrivateSpaceExist()) {
+            return true;
+        }
+        // a name indicating that the profile was created from the PS Settings page
+        final String userName = "psSettingsUser";
+
+        if (mUserHandle == null) {
+            try {
+                mUserHandle = mUserManager.createProfile(
+                                userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>());
+            } catch (Exception e) {
+                Log.e(TAG, "Error creating private space", e);
+                return false;
+            }
+
+            if (mUserHandle == null) {
+                Log.e(TAG, "Failed to create private space");
+                return false;
+            }
+
+            IActivityManager am = ActivityManager.getService();
+            try {
+                am.startProfile(mUserHandle.getIdentifier());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to start private profile");
+                return false;
+            }
+
+            Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
+        }
+        return true;
+    }
+
+    /** Returns the {@link ErrorDeletingPrivateSpace} enum representing the result of operation.
+     *
+     * <p> This method should be used ONLY by the delete-PS controller in the PS Settings page.
+     */
+    public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() {
+        if (!doesPrivateSpaceExist()) {
+            return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE;
+        }
+
+        try {
+            Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier());
+            if (mUserManager.removeUser(mUserHandle)) {
+                Log.i(TAG, "Private space deleted");
+                mUserHandle = null;
+
+                return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
+            } else {
+                Log.e(TAG, "Failed to delete private space");
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Error deleting private space", e);
+        }
+        return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
+    }
+
+    /** Returns true if the Private space exists. */
+    public synchronized boolean doesPrivateSpaceExist() {
+        if (mUserHandle != null) {
+            return true;
+        }
+
+        List<UserInfo> users = mUserManager.getProfiles(0);
+        for (UserInfo user : users) {
+            if (user.isPrivateProfile()) {
+                mUserHandle = user.getUserHandle();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static synchronized PrivateSpaceMaintainer getInstance(Context context) {
+        if (sPrivateSpaceMaintainer == null) {
+            sPrivateSpaceMaintainer = new PrivateSpaceMaintainer(context);
+        }
+        return sPrivateSpaceMaintainer;
+    }
+
+    private PrivateSpaceMaintainer(Context context) {
+        mContext = context.getApplicationContext();
+        mUserManager = mContext.getSystemService(UserManager.class);
+    }
+}
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java
new file mode 100644
index 0000000..b07c623
--- /dev/null
+++ b/src/com/android/settings/privatespace/PrivateSpaceSafetySource.java
@@ -0,0 +1,106 @@
+/*
+ * 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.privatespace;
+
+import android.app.PendingIntent;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserManager;
+import android.safetycenter.SafetyEvent;
+import android.safetycenter.SafetySourceData;
+import android.safetycenter.SafetySourceStatus;
+import android.util.FeatureFlagUtils;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
+import com.android.settingslib.transition.SettingsTransitionHelper;
+
+/** Private Space safety source for the Safety Center */
+public final class PrivateSpaceSafetySource {
+    public static final String SAFETY_SOURCE_ID = "AndroidPrivateSpace";
+    private static final String TAG = "PrivateSpaceSafetySource";
+
+    private PrivateSpaceSafetySource() {}
+
+    /** Sets lock screen safety data for Safety Center. */
+    public static void setSafetySourceData(Context context,
+            SafetyEvent safetyEvent) {
+        if (!SafetyCenterManagerWrapper.get().isEnabled(context)) {
+            Log.i(TAG, "Safety Center disabled");
+            return;
+        }
+
+        // Check the profile type - we don't want to show this for anything other than primary user.
+        UserManager userManager = context.getSystemService(UserManager.class);
+        if (userManager != null && !userManager.isMainUser()) {
+            Log.i(TAG, "setSafetySourceData not main user");
+            return;
+        }
+
+        // Temporary workaround to help prevent the PS Settings showing up in droidfood builds.
+        // TODO(b/295516544): remove this when the trunk stable feature flag for PS is available.
+        if (!FeatureFlagUtils.isEnabled(context,
+                FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS)) {
+            // Setting null safetySourceData so that an old entry gets cleared out and this way
+            // provide a response since SC always expects one on rescan.
+            SafetyCenterManagerWrapper.get().setSafetySourceData(
+                    context,
+                    SAFETY_SOURCE_ID,
+                    /* safetySourceData */ null,
+                    safetyEvent
+            );
+            return;
+        }
+
+        PendingIntent pendingIntent = getPendingIntentForPsDashboard(context);
+
+        SafetySourceStatus status = new SafetySourceStatus.Builder(
+                context.getString(R.string.private_space_title),
+                context.getString(R.string.private_space_summary),
+                SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED)
+                .setPendingIntent(pendingIntent).build();
+        SafetySourceData safetySourceData =
+                new SafetySourceData.Builder().setStatus(status).build();
+
+        Log.d(TAG, "Setting safety source data");
+        SafetyCenterManagerWrapper.get().setSafetySourceData(
+                context,
+                SAFETY_SOURCE_ID,
+                safetySourceData,
+                safetyEvent
+        );
+    }
+
+    private static PendingIntent getPendingIntentForPsDashboard(Context context) {
+        Intent privateSpaceDashboardIntent = new SubSettingLauncher(context)
+                .setDestination(PrivateSpaceDashboardFragment.class.getName())
+                .setTransitionType(SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE)
+                .setSourceMetricsCategory(SettingsEnums.PRIVATE_SPACE_SETTINGS)
+                .toIntent()
+                .setIdentifier(SAFETY_SOURCE_ID);
+
+        return PendingIntent
+                .getActivity(
+                        context,
+                        /* requestCode */ 0,
+                        privateSpaceDashboardIntent,
+                        PendingIntent.FLAG_IMMUTABLE);
+    }
+}
diff --git a/src/com/android/settings/privatespace/UseOneLockController.java b/src/com/android/settings/privatespace/UseOneLockController.java
new file mode 100644
index 0000000..a94db57
--- /dev/null
+++ b/src/com/android/settings/privatespace/UseOneLockController.java
@@ -0,0 +1,50 @@
+/*
+ * 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.privatespace;
+
+import android.content.Context;
+
+import com.android.settings.core.TogglePreferenceController;
+
+/** Represents the preference controller for using the same lock as the screen lock */
+public class UseOneLockController extends TogglePreferenceController {
+    public UseOneLockController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public boolean isChecked() {
+        // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider
+        return false;
+    }
+
+    @Override
+    public boolean setChecked(boolean isChecked) {
+        // TODO(b/293569406) Need to save this to a persistent store, maybe like SettingsProvider
+        return true;
+    }
+
+    @Override
+    public int getSliceHighlightMenuRes() {
+        return 0;
+    }
+}
diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java
index 0b556e7..cc0f892 100644
--- a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java
+++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java
@@ -28,6 +28,7 @@
 import android.safetycenter.SafetyCenterManager;
 import android.safetycenter.SafetyEvent;
 
+import com.android.settings.privatespace.PrivateSpaceSafetySource;
 import com.android.settings.security.ScreenLockPreferenceDetailsUtils;
 
 import com.google.common.collect.ImmutableList;
@@ -79,11 +80,16 @@
         if (sourceIds.contains(BiometricsSafetySource.SAFETY_SOURCE_ID)) {
             BiometricsSafetySource.setSafetySourceData(context, safetyEvent);
         }
+
+        if (sourceIds.contains(PrivateSpaceSafetySource.SAFETY_SOURCE_ID)) {
+            PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent);
+        }
     }
 
     private static void refreshAllSafetySources(Context context, SafetyEvent safetyEvent) {
         LockScreenSafetySource.setSafetySourceData(context,
                 new ScreenLockPreferenceDetailsUtils(context), safetyEvent);
         BiometricsSafetySource.setSafetySourceData(context, safetyEvent);
+        PrivateSpaceSafetySource.setSafetySourceData(context, safetyEvent);
     }
 }
diff --git a/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.java b/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.java
new file mode 100644
index 0000000..8fb3eae
--- /dev/null
+++ b/tests/unit/src/com/android/settings/privatespace/DeletePrivateSpaceControllerTest.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.privatespace;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
+import static com.android.settings.privatespace.PrivateSpaceMaintainer.ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class DeletePrivateSpaceControllerTest {
+    @Mock private PrivateSpaceMaintainer mPrivateSpaceMaintainer;
+    @Mock private Context mContext;
+
+    private Preference mPreference;
+    private DeletePrivateSpaceController mDeletePrivateSpaceController;
+
+    /** Required setup before a test. */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
+        final String preferenceKey = "private_space_delete";
+
+        mPreference = new Preference(ApplicationProvider.getApplicationContext());
+        mPreference.setKey(preferenceKey);
+
+        mDeletePrivateSpaceController =
+                new DeletePrivateSpaceController(
+                        mContext,
+                        preferenceKey,
+                        new DeletePrivateSpaceController.Injector() {
+                            @Override
+                            PrivateSpaceMaintainer injectPrivateSpaceMaintainer(Context context) {
+                                return mPrivateSpaceMaintainer;
+                            }
+                        });
+    }
+
+    /** Tests that the controller is always available. */
+    @Test
+    public void getAvailabilityStatus_returnsAvailable() {
+        assertThat(mDeletePrivateSpaceController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    /** Tests that on click it attempts to delete the PS. */
+    @Test
+    public void handlePreferenceTreeClick_attemptsToDeletePrivateSpace() {
+        doReturn(DELETE_PS_ERROR_NONE).when(mPrivateSpaceMaintainer).deletePrivateSpace();
+        DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController);
+        doNothing().when(spy).showSuccessfulDeletionToast();
+        spy.handlePreferenceTreeClick(mPreference);
+
+        verify(mPrivateSpaceMaintainer).deletePrivateSpace();
+    }
+
+    /** Tests that on deletion of PS relevant toast is shown. */
+    @Test
+    public void handlePreferenceTreeClick_onDeletion_showsDeletedToast() {
+        doReturn(DELETE_PS_ERROR_NONE).when(mPrivateSpaceMaintainer).deletePrivateSpace();
+        DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController);
+        doNothing().when(spy).showSuccessfulDeletionToast();
+        spy.handlePreferenceTreeClick(mPreference);
+
+        verify(spy).showSuccessfulDeletionToast();
+    }
+
+    /** Tests that on failing to delete the PS relevant toast is shown. */
+    @Test
+    public void handlePreferenceTreeClick_onDeletionError_showsDeletionFailedToast() {
+        doReturn(DELETE_PS_ERROR_INTERNAL).when(mPrivateSpaceMaintainer).deletePrivateSpace();
+        DeletePrivateSpaceController spy = Mockito.spy(mDeletePrivateSpaceController);
+        doNothing().when(spy).showDeletionInternalErrorToast();
+        spy.handlePreferenceTreeClick(mPreference);
+
+        verify(spy).showDeletionInternalErrorToast();
+    }
+}
diff --git a/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java b/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java
new file mode 100644
index 0000000..1a1769e
--- /dev/null
+++ b/tests/unit/src/com/android/settings/privatespace/HidePrivateSpaceControllerTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.privatespace;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class HidePrivateSpaceControllerTest {
+    @Mock private Context mContext;
+    private HidePrivateSpaceController mHidePrivateSpaceController;
+
+    /** Required setup before a test. */
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        final String preferenceKey = "private_space_hidden";
+
+        mHidePrivateSpaceController = new HidePrivateSpaceController(mContext, preferenceKey);
+    }
+
+    /** Tests that the controller is always available. */
+    @Test
+    public void getAvailabilityStatus_returnsAvailable() {
+        assertThat(mHidePrivateSpaceController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java
new file mode 100644
index 0000000..2dc00e1
--- /dev/null
+++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceSafetySourceTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.privatespace;
+
+
+import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED;
+import static com.android.settings.privatespace.PrivateSpaceSafetySource.SAFETY_SOURCE_ID;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.safetycenter.SafetyEvent;
+import android.safetycenter.SafetySourceData;
+import android.safetycenter.SafetySourceStatus;
+import android.util.FeatureFlagUtils;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class PrivateSpaceSafetySourceTest {
+    private static final SafetyEvent EVENT_TYPE_DEVICE_REBOOTED =
+            new SafetyEvent.Builder(SAFETY_EVENT_TYPE_DEVICE_REBOOTED).build();
+    private Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper;
+
+    /** Required setup before a test. */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper;
+
+        FeatureFlagUtils
+                .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, true);
+    }
+
+    /** Required setup after a test. */
+    @After
+    public void tearDown() {
+        SafetyCenterManagerWrapper.sInstance = null;
+    }
+
+    /** Tests that when SC is disabled we don't set any data. */
+    @Test
+    public void onDeviceRebootedEvent_whenSafetyCenterDisabled_doesNotSetData() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(false);
+
+        PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED);
+
+        verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData(
+                any(), any(), any(), any());
+    }
+
+    /** Tests that when SC is enabled we set data. */
+    @Test
+    public void onDeviceRebootedEvent_whenSafetyCenterEnabled_setsData() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true);
+
+        PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED);
+
+        verify(mSafetyCenterManagerWrapper).setSafetySourceData(
+                any(), eq(SAFETY_SOURCE_ID), any(), eq(EVENT_TYPE_DEVICE_REBOOTED));
+    }
+
+    // TODO(b/295516544): Modify this test for the new trunk stable flag instead when available.
+    /** Tests that when the feature is disabled null data is set. */
+    @Test
+    public void setSafetySourceData_whenFeatureDisabled_setsNullData() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true);
+        FeatureFlagUtils
+                .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, false);
+
+        PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED);
+
+        ArgumentCaptor<SafetySourceData> captor = ArgumentCaptor.forClass(SafetySourceData.class);
+        verify(mSafetyCenterManagerWrapper).setSafetySourceData(
+                any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED));
+        SafetySourceData safetySourceData = captor.getValue();
+        assertThat(safetySourceData).isNull();
+
+        FeatureFlagUtils
+                .setEnabled(mContext, FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS, true);
+    }
+
+    /** Tests that setSafetySourceData sets the source status enabled. */
+    @Test
+    public void setSafetySourceData_setsEnabled() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true);
+
+        PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED);
+
+        ArgumentCaptor<SafetySourceData> captor = ArgumentCaptor.forClass(SafetySourceData.class);
+        verify(mSafetyCenterManagerWrapper).setSafetySourceData(
+                any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED));
+        SafetySourceData safetySourceData = captor.getValue();
+        SafetySourceStatus safetySourceStatus = safetySourceData.getStatus();
+        assertThat(safetySourceStatus.isEnabled()).isTrue();
+    }
+
+    /** Tests that setSafetySourceData sets the PS settings page intent. */
+    @Test
+    public void setSafetySourceData_setsPsIntent() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mContext)).thenReturn(true);
+
+        PrivateSpaceSafetySource.setSafetySourceData(mContext, EVENT_TYPE_DEVICE_REBOOTED);
+
+        ArgumentCaptor<SafetySourceData> captor = ArgumentCaptor.forClass(SafetySourceData.class);
+        verify(mSafetyCenterManagerWrapper).setSafetySourceData(
+                any(), eq(SAFETY_SOURCE_ID), captor.capture(), eq(EVENT_TYPE_DEVICE_REBOOTED));
+        SafetySourceData safetySourceData = captor.getValue();
+        SafetySourceStatus safetySourceStatus = safetySourceData.getStatus();
+        assertThat(safetySourceStatus.getPendingIntent().getIntent().getIdentifier())
+                .isEqualTo(SAFETY_SOURCE_ID);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java b/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java
new file mode 100644
index 0000000..e7ebb37
--- /dev/null
+++ b/tests/unit/src/com/android/settings/privatespace/UseOneLockControllerTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.privatespace;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class UseOneLockControllerTest {
+    @Mock private Context mContext;
+    private UseOneLockController mUseOneLockController;
+
+    /** Required setup before a test. */
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        final String preferenceKey = "private_space_use_one_lock";
+
+        mUseOneLockController = new UseOneLockController(mContext, preferenceKey);
+    }
+
+    /** Tests that the controller is always available. */
+    @Test
+    public void getAvailabilityStatus_returnsAvailable() {
+        assertThat(mUseOneLockController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java
index 3ad1874..caae44a 100644
--- a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java
+++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java
@@ -21,9 +21,7 @@
 import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS;
 import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED;
 import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED;
-
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -33,11 +31,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.safetycenter.SafetyEvent;
+import android.safetycenter.SafetySourceData;
+import android.util.FeatureFlagUtils;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.privatespace.PrivateSpaceSafetySource;
 import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.After;
@@ -216,6 +217,62 @@
         assertThat(captor.getValue()).isEqualTo(BiometricsSafetySource.SAFETY_SOURCE_ID);
     }
 
+    /**
+     *  Tests that on receiving the refresh broadcast request with the PS source id, the PS data
+     * is set.
+     */
+    @Test
+    public void onReceive_onRefresh_withPrivateSpaceSourceId_setsPrivateSpaceData() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true);
+        Intent intent =
+                new Intent()
+                        .setAction(ACTION_REFRESH_SAFETY_SOURCES)
+                        .putExtra(
+                                EXTRA_REFRESH_SAFETY_SOURCE_IDS,
+                                new String[] {PrivateSpaceSafetySource.SAFETY_SOURCE_ID})
+                        .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID);
+
+        new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent);
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+        verify(mSafetyCenterManagerWrapper, times(1))
+                .setSafetySourceData(any(), captor.capture(), any(), any());
+
+        assertThat(captor.getValue()).isEqualTo(PrivateSpaceSafetySource.SAFETY_SOURCE_ID);
+    }
+
+    /** Tests that the PS source sets null data when it's disabled. */
+    // TODO(b/295516544): Modify this test for the new trunk stable flag instead when available.
+    @Test
+    public void onReceive_onRefresh_withPrivateSpaceFeatureDisabled_setsNullData() {
+        when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true);
+        FeatureFlagUtils
+                .setEnabled(
+                        mApplicationContext,
+                        FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS,
+                        false);
+
+        Intent intent =
+                new Intent()
+                        .setAction(ACTION_REFRESH_SAFETY_SOURCES)
+                        .putExtra(
+                                EXTRA_REFRESH_SAFETY_SOURCE_IDS,
+                                new String[] {PrivateSpaceSafetySource.SAFETY_SOURCE_ID})
+                        .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID);
+
+        new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent);
+        ArgumentCaptor<SafetySourceData> captor = ArgumentCaptor.forClass(SafetySourceData.class);
+        verify(mSafetyCenterManagerWrapper, times(1))
+                .setSafetySourceData(any(), any(), captor.capture(), any());
+
+        assertThat(captor.getValue()).isEqualTo(null);
+
+        FeatureFlagUtils
+                .setEnabled(
+                        mApplicationContext,
+                        FeatureFlagUtils.SETTINGS_PRIVATE_SPACE_SETTINGS,
+                        true);
+    }
+
     @Test
     public void onReceive_onBootCompleted_setsBootCompleteEvent() {
         when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true);
@@ -223,22 +280,22 @@
 
         new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent);
         ArgumentCaptor<SafetyEvent> captor = ArgumentCaptor.forClass(SafetyEvent.class);
-        verify(mSafetyCenterManagerWrapper, times(2))
+        verify(mSafetyCenterManagerWrapper, times(3))
                 .setSafetySourceData(any(), any(), any(), captor.capture());
 
         SafetyEvent bootEvent = new SafetyEvent.Builder(SAFETY_EVENT_TYPE_DEVICE_REBOOTED).build();
         assertThat(captor.getAllValues())
-                .containsExactlyElementsIn(Arrays.asList(bootEvent, bootEvent));
+                .containsExactlyElementsIn(Arrays.asList(bootEvent, bootEvent, bootEvent));
     }
 
     @Test
-    public void onReceive_onBootCompleted_sendsBiometricAndLockscreenData() {
+    public void onReceive_onBootCompleted_sendsAllSafetySourcesData() {
         when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true);
         Intent intent = new Intent().setAction(Intent.ACTION_BOOT_COMPLETED);
 
         new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent);
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
-        verify(mSafetyCenterManagerWrapper, times(2))
+        verify(mSafetyCenterManagerWrapper, times(3))
                 .setSafetySourceData(any(), captor.capture(), any(), any());
         List<String> safetySourceIdList = captor.getAllValues();
 
@@ -246,5 +303,7 @@
                 id -> id.equals(LockScreenSafetySource.SAFETY_SOURCE_ID))).isTrue();
         assertThat(safetySourceIdList.stream().anyMatch(
                 id -> id.equals(BiometricsSafetySource.SAFETY_SOURCE_ID))).isTrue();
+        assertThat(safetySourceIdList.stream().anyMatch(
+                id -> id.equals(PrivateSpaceSafetySource.SAFETY_SOURCE_ID))).isTrue();
     }
 }