Merge "Set preferred network types on background thread" into main
diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig
index 84bb578..600a0af 100644
--- a/aconfig/settings_connecteddevice_flag_declarations.aconfig
+++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig
@@ -9,20 +9,6 @@
 }
 
 flag {
-  name: "enable_le_audio_sharing"
-  namespace: "pixel_cross_device_control"
-  description: "Gates whether to enable LE audio sharing"
-  bug: "305620450"
-}
-
-flag {
-  name: "enable_le_audio_qr_code_private_broadcast_sharing"
-  namespace: "pixel_cross_device_control"
-  description: "Gates whether to enable LE audio private broadcast sharing via QR code"
-  bug: "308368124"
-}
-
-flag {
   name: "enable_auth_challenge_for_usb_preferences"
   namespace: "safety_center"
   description: "Gates whether to require an auth challenge for changing USB preferences"
@@ -41,4 +27,4 @@
   namespace: "dck_framework"
   description: "Hide exclusively managed Bluetooth devices in BT settings menu."
   bug: "322285078"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 107423f..46b7e86 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2110,6 +2110,14 @@
     <string name="wifi_ip_settings">IP settings</string>
     <!-- Label for the spinner to show Wifi MAC randomization [CHAR LIMIT=25] -->
     <string name="wifi_privacy_settings">Privacy</string>
+    <!-- Category title for the spinner to show Wifi MAC randomization [CHAR LIMIT=25] -->
+    <string name="wifi_privacy_mac_settings">MAC</string>
+    <!-- Category title for Device name [CHAR LIMIT=25] -->
+    <string name="wifi_privacy_device_name_settings">Device name</string>
+    <!-- Toggle button title for allowing/disallowing sending device name to DHCP [CHAR LIMIT=50] -->
+    <string name="wifi_privacy_send_device_name_toggle_title">Send device name</string>
+    <!-- Toggle button title for allowing/disallowing sending device name to DHCP [CHAR LIMIT=50] -->
+    <string name="wifi_privacy_send_device_name_toggle_summary">Share this device\u0027s name with the network</string>
     <!-- Label for the subscription preference. [CHAR LIMIT=32] -->
     <string name="wifi_subscription">Subscription</string>
     <!-- Summary text for the subscription preference. [CHAR LIMIT=NONE] -->
diff --git a/res/xml/wifi_network_details_fragment2.xml b/res/xml/wifi_network_details_fragment2.xml
index daff20f..598f9d8 100644
--- a/res/xml/wifi_network_details_fragment2.xml
+++ b/res/xml/wifi_network_details_fragment2.xml
@@ -97,6 +97,11 @@
         android:entries="@array/wifi_privacy_entries"
         android:entryValues="@array/wifi_privacy_values"/>
 
+    <com.android.settings.spa.preference.ComposePreference
+        android:key="privacy_settings"
+        android:title="@string/wifi_privacy_settings"
+        settings:controller="com.android.settings.wifi.details2.WifiPrivacyPreferenceController"/>
+
     <Preference
         android:key="subscription_detail"
         android:title="@string/wifi_subscription"
diff --git a/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.kt b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.kt
index 1ed9d9a..d709574 100644
--- a/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.kt
+++ b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.kt
@@ -80,8 +80,6 @@
     @VisibleForTesting
     var progressDialog: ProgressDialog? = null
 
-    private lateinit var preference: Preference
-
     private var subId by notNull<Int>()
 
     /**
@@ -99,11 +97,6 @@
         if (MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId)) AVAILABLE
         else CONDITIONALLY_UNAVAILABLE
 
-    override fun displayPreference(screen: PreferenceScreen) {
-        super.displayPreference(screen)
-        preference = screen.findPreference(preferenceKey)!!
-    }
-
     @Composable
     override fun Content() {
         val coroutineScope = rememberCoroutineScope()
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 7a1d915..568188f 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -56,6 +56,7 @@
 import com.android.settings.spa.system.AppLanguagesPageProvider
 import com.android.settings.spa.system.LanguageAndInputPageProvider
 import com.android.settings.spa.system.SystemMainPageProvider
+import com.android.settings.wifi.details2.WifiPrivacyPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironment
 import com.android.settingslib.spa.framework.common.SpaLogger
@@ -122,6 +123,7 @@
         SimOnboardingPageProvider,
         BatteryOptimizationModeAppListPageProvider,
         NetworkCellularGroupProvider,
+        WifiPrivacyPageProvider,
     )
 
     override val logger = if (FeatureFlagUtils.isEnabled(
diff --git a/src/com/android/settings/spa/preference/ComposePreferenceController.kt b/src/com/android/settings/spa/preference/ComposePreferenceController.kt
index 9dd8282..5ba1d24 100644
--- a/src/com/android/settings/spa/preference/ComposePreferenceController.kt
+++ b/src/com/android/settings/spa/preference/ComposePreferenceController.kt
@@ -24,7 +24,7 @@
 abstract class ComposePreferenceController(context: Context, preferenceKey: String) :
     BasePreferenceController(context, preferenceKey) {
 
-    private lateinit var preference: ComposePreference
+    protected lateinit var preference: ComposePreference
 
     override fun displayPreference(screen: PreferenceScreen) {
         super.displayPreference(screen)
diff --git a/src/com/android/settings/wifi/WepNetworksPreferenceController.kt b/src/com/android/settings/wifi/WepNetworksPreferenceController.kt
index 6263bfd..c84e79a 100644
--- a/src/com/android/settings/wifi/WepNetworksPreferenceController.kt
+++ b/src/com/android/settings/wifi/WepNetworksPreferenceController.kt
@@ -39,15 +39,8 @@
 class WepNetworksPreferenceController(context: Context, preferenceKey: String) :
     ComposePreferenceController(context, preferenceKey) {
 
-    private lateinit var preference: Preference
-
     var wifiManager = context.getSystemService(WifiManager::class.java)!!
 
-    override fun displayPreference(screen: PreferenceScreen) {
-        super.displayPreference(screen)
-        preference = screen.findPreference(preferenceKey)!!
-    }
-
     override fun getAvailabilityStatus() = if (Flags.androidVWifiApi()) AVAILABLE
     else UNSUPPORTED_ON_DEVICE
 
diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
index 0384f0d..e1774e3 100644
--- a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
+++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java
@@ -58,6 +58,7 @@
 import com.android.settings.wifi.details2.WifiAutoConnectPreferenceController2;
 import com.android.settings.wifi.details2.WifiDetailPreferenceController2;
 import com.android.settings.wifi.details2.WifiMeteredPreferenceController2;
+import com.android.settings.wifi.details2.WifiPrivacyPreferenceController;
 import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
 import com.android.settings.wifi.details2.WifiSecondSummaryController2;
 import com.android.settings.wifi.details2.WifiSubscriptionDetailPreferenceController2;
@@ -119,6 +120,13 @@
     }
 
     @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        use(WifiPrivacyPreferenceController.class)
+                .setWifiEntryKey(getArguments().getString(KEY_CHOSEN_WIFIENTRY_KEY));
+    }
+
+    @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
         setIfOnlyAvailableForAdmins(true);
diff --git a/src/com/android/settings/wifi/details2/WifiPrivacyPageProvider.kt b/src/com/android/settings/wifi/details2/WifiPrivacyPageProvider.kt
new file mode 100644
index 0000000..e41863c
--- /dev/null
+++ b/src/com/android/settings/wifi/details2/WifiPrivacyPageProvider.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 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.wifi.details2
+
+import android.content.Context
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Process
+import android.os.SimpleClock
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.stringArrayResource
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import com.android.settings.R
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.ListPreferenceModel
+import com.android.settingslib.spa.widget.preference.ListPreferenceOption
+import com.android.settingslib.spa.widget.preference.RadioPreferences
+import com.android.settingslib.spa.widget.preference.SwitchPreference
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CategoryTitle
+import com.android.wifitrackerlib.WifiEntry
+import java.time.Clock
+import java.time.ZoneOffset
+
+const val WIFI_ENTRY_KEY = "wifiEntryKey"
+
+object WifiPrivacyPageProvider : SettingsPageProvider {
+    override val name = "WifiPrivacy"
+    const val TAG = "WifiPrivacyPageProvider"
+
+    override val parameter = listOf(
+        navArgument(WIFI_ENTRY_KEY) { type = NavType.StringType },
+    )
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        val wifiEntryKey = arguments!!.getString(WIFI_ENTRY_KEY)
+        if (wifiEntryKey != null) {
+            val context = LocalContext.current
+            val lifecycle = LocalLifecycleOwner.current.lifecycle
+            val wifiEntry = remember {
+                getWifiEntry(context, wifiEntryKey, lifecycle)
+            }
+            WifiPrivacyPage(wifiEntry)
+        }
+    }
+
+    fun getRoute(
+        wifiEntryKey: String,
+    ): String = "${name}/$wifiEntryKey"
+}
+
+@Composable
+fun WifiPrivacyPage(wifiEntry: WifiEntry) {
+    val isSelectable: Boolean = wifiEntry.canSetPrivacy()
+    RegularScaffold(
+        title = stringResource(id = R.string.wifi_privacy_settings)
+    ) {
+        Column {
+            val title = stringResource(id = R.string.wifi_privacy_mac_settings)
+            val wifiPrivacyEntries = stringArrayResource(R.array.wifi_privacy_entries)
+            val wifiPrivacyValues = stringArrayResource(R.array.wifi_privacy_values)
+            val textsSelectedId = rememberSaveable { mutableIntStateOf(wifiEntry.privacy) }
+            val dataList = remember {
+                wifiPrivacyEntries.mapIndexed { index, text ->
+                    ListPreferenceOption(id = wifiPrivacyValues[index].toInt(), text = text)
+                }
+            }
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = title
+                    override val options = dataList
+                    override val selectedId = textsSelectedId
+                    override val onIdSelected: (Int) -> Unit = {
+                        textsSelectedId.intValue = it
+                        onSelectedChange(wifiEntry, it)
+                    }
+                    override val enabled = { isSelectable }
+                }
+            })
+            wifiEntry.wifiConfiguration?.let {
+                DeviceNameSwitchPreference(it)
+            }
+        }
+    }
+}
+
+@Composable
+fun DeviceNameSwitchPreference(wifiConfiguration: WifiConfiguration){
+    Spacer(modifier = Modifier.width(SettingsDimension.itemDividerHeight))
+    CategoryTitle(title = stringResource(R.string.wifi_privacy_device_name_settings))
+    Spacer(modifier = Modifier.width(SettingsDimension.itemDividerHeight))
+    var checked by remember {
+        mutableStateOf(wifiConfiguration.isSendDhcpHostnameEnabled)
+    }
+    val context = LocalContext.current
+    val wifiManager = context.getSystemService(WifiManager::class.java)!!
+    SwitchPreference(object : SwitchPreferenceModel {
+        override val title =
+            context.resources.getString(
+                R.string.wifi_privacy_send_device_name_toggle_title
+            )
+        override val summary =
+            {
+                context.resources.getString(
+                    R.string.wifi_privacy_send_device_name_toggle_summary
+                )
+            }
+        override val checked = { checked }
+        override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
+            wifiConfiguration.isSendDhcpHostnameEnabled = newChecked
+            wifiManager.save(wifiConfiguration, null /* listener */)
+            checked = newChecked
+        }
+    })
+}
+
+fun onSelectedChange(wifiEntry: WifiEntry, privacy: Int) {
+    if (wifiEntry.privacy == privacy) {
+        // Prevent disconnection + reconnection if settings not changed.
+        return
+    }
+    wifiEntry.setPrivacy(privacy)
+
+    // To activate changing, we need to reconnect network. WiFi will auto connect to
+    // current network after disconnect(). Only needed when this is connected network.
+
+    // To activate changing, we need to reconnect network. WiFi will auto connect to
+    // current network after disconnect(). Only needed when this is connected network.
+    if (wifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) {
+        wifiEntry.disconnect(null /* callback */)
+        wifiEntry.connect(null /* callback */)
+    }
+}
+
+fun getWifiEntry(
+    context: Context,
+    wifiEntryKey: String,
+    liftCycle: androidx.lifecycle.Lifecycle
+): WifiEntry {
+    // Max age of tracked WifiEntries
+    val MAX_SCAN_AGE_MILLIS: Long = 15000
+    // Interval between initiating SavedNetworkTracker scans
+    val SCAN_INTERVAL_MILLIS: Long = 10000
+    val mWorkerThread = HandlerThread(
+        WifiPrivacyPageProvider.TAG,
+        Process.THREAD_PRIORITY_BACKGROUND
+    )
+    mWorkerThread.start()
+    val elapsedRealtimeClock: Clock = object : SimpleClock(ZoneOffset.UTC) {
+        override fun millis(): Long {
+            return android.os.SystemClock.elapsedRealtime()
+        }
+    }
+    val mNetworkDetailsTracker = featureFactory
+        .wifiTrackerLibProvider
+        .createNetworkDetailsTracker(
+            liftCycle,
+            context,
+            Handler(Looper.getMainLooper()),
+            mWorkerThread.getThreadHandler(),
+            elapsedRealtimeClock,
+            MAX_SCAN_AGE_MILLIS,
+            SCAN_INTERVAL_MILLIS,
+            wifiEntryKey
+        )
+    return mNetworkDetailsTracker.wifiEntry
+}
diff --git a/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController.kt b/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController.kt
new file mode 100644
index 0000000..42741e3
--- /dev/null
+++ b/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 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.wifi.details2
+
+import android.content.Context
+import android.net.wifi.WifiManager
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import com.android.settings.R
+import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
+import com.android.settings.spa.preference.ComposePreferenceController
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.wifi.flags.Flags
+
+class WifiPrivacyPreferenceController(context: Context, preferenceKey: String) :
+    ComposePreferenceController(context, preferenceKey) {
+
+    private var wifiEntryKey: String? = null
+
+    var wifiManager = context.getSystemService(WifiManager::class.java)!!
+
+    fun setWifiEntryKey(key: String?) {
+        wifiEntryKey = key
+    }
+
+    override fun getAvailabilityStatus() =
+        if (Flags.androidVWifiApi() && wifiManager.isConnectedMacRandomizationSupported) AVAILABLE
+        else CONDITIONALLY_UNAVAILABLE
+
+    @Composable
+    override fun Content() {
+        Preference(object : PreferenceModel {
+            override val title = stringResource(R.string.wifi_privacy_settings)
+            override val icon = @Composable {
+                Icon(
+                    ImageVector.vectorResource(R.drawable.ic_wifi_privacy_24dp),
+                    contentDescription = null
+                )
+            }
+            override val onClick: () -> Unit =
+                {
+                    wifiEntryKey?.let {
+                        mContext.startSpaActivity(WifiPrivacyPageProvider.getRoute(it))
+                    }
+                }
+        })
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController2.java
index 8c78e80..5d393e5 100644
--- a/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceController2.java
@@ -26,6 +26,7 @@
 
 import com.android.settings.R;
 import com.android.settings.core.BasePreferenceController;
+import com.android.wifi.flags.Flags;
 import com.android.wifitrackerlib.WifiEntry;
 
 /**
@@ -50,7 +51,7 @@
 
     @Override
     public int getAvailabilityStatus() {
-        return mWifiManager.isConnectedMacRandomizationSupported()
+        return (!Flags.androidVWifiApi() && mWifiManager.isConnectedMacRandomizationSupported())
                 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
     }
 
diff --git a/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPageProviderTest.kt
new file mode 100644
index 0000000..5c9a1a4
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPageProviderTest.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 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.wifi.details2
+
+import android.content.Context
+import android.net.wifi.WifiConfiguration
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertIsSelectable
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.wifitrackerlib.WifiEntry
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class WifiPrivacyPageProviderTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private var mockWifiConfiguration = mock<WifiConfiguration>() {
+        on { isSendDhcpHostnameEnabled } doReturn true
+    }
+    private var mockWifiEntry = mock<WifiEntry>() {
+        on { canSetPrivacy() } doReturn true
+        on { privacy } doReturn 0
+        on { wifiConfiguration } doReturn mockWifiConfiguration
+    }
+
+    @Test
+    fun apnEditPageProvider_name() {
+        Truth.assertThat(WifiPrivacyPageProvider.name).isEqualTo("WifiPrivacy")
+    }
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_settings)
+        ).assertIsDisplayed()
+    }
+
+    @Test
+    fun category_mac_title_displayed() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_mac_settings)
+        ).assertIsDisplayed()
+    }
+
+    @Test
+    fun category_mac_list_displayed() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        val wifiPrivacyEntries = context.resources.getStringArray(R.array.wifi_privacy_entries)
+        for (entry in wifiPrivacyEntries) {
+            composeTestRule.onNodeWithText(
+                entry
+            ).assertIsDisplayed()
+        }
+    }
+
+    @Test
+    fun category_mac_list_selectable() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        val wifiPrivacyEntries = context.resources.getStringArray(R.array.wifi_privacy_entries)
+        for (entry in wifiPrivacyEntries) {
+            composeTestRule.onNodeWithText(
+                entry
+            ).assertIsSelectable()
+        }
+    }
+
+    @Test
+    fun category_mac_list_default_selected() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        val wifiPrivacyEntries = context.resources.getStringArray(R.array.wifi_privacy_entries)
+        val wifiPrivacyValues = context.resources.getStringArray(R.array.wifi_privacy_values)
+        composeTestRule.onNodeWithText(
+            wifiPrivacyEntries[wifiPrivacyValues.indexOf("0")]
+        ).assertIsSelected()
+    }
+
+    @Test
+    fun category_mac_list_not_enabled() {
+        mockWifiEntry.stub {
+            on { canSetPrivacy() } doReturn false
+        }
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        val wifiPrivacyEntries = context.resources.getStringArray(R.array.wifi_privacy_entries)
+        for (entry in wifiPrivacyEntries) {
+            composeTestRule.onNodeWithText(entry).assertIsNotEnabled()
+        }
+    }
+
+    @Test
+    fun category_send_device_name_title_displayed() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_device_name_settings)
+        ).assertIsDisplayed()
+    }
+
+    @Test
+    fun toggle_send_device_name_title_displayed() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_send_device_name_toggle_title)
+        ).assertIsDisplayed()
+    }
+
+    @Test
+    fun send_device_name_turnOn() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_send_device_name_toggle_title)
+        ).assertIsOn()
+    }
+
+    @Test
+    fun onClick_turnOff() {
+        composeTestRule.setContent {
+            WifiPrivacyPage(mockWifiEntry)
+        }
+
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_send_device_name_toggle_title)
+        ).performClick()
+
+        composeTestRule.onNodeWithText(
+            context.getString(R.string.wifi_privacy_send_device_name_toggle_title)
+        ).assertIsOff()
+    }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceControllerTest.kt
new file mode 100644
index 0000000..98997e4
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/wifi/details2/WifiPrivacyPreferenceControllerTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 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.wifi.details2
+
+import android.content.Context
+import android.content.Intent
+import android.net.wifi.WifiManager
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.core.os.bundleOf
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.framework.util.KEY_DESTINATION
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class WifiPrivacyPreferenceControllerTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val mockWifiManager = mock<WifiManager> {
+        on { isConnectedMacRandomizationSupported } doReturn true
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { getSystemService(WifiManager::class.java) } doReturn mockWifiManager
+        doNothing().whenever(mock).startActivity(any())
+    }
+
+    private val controller = WifiPrivacyPreferenceController(context, TEST_KEY)
+
+    @Test
+    fun title_isDisplayed() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                controller.Content()
+            }
+        }
+
+        composeTestRule.onNodeWithText(context.getString(R.string.wifi_privacy_settings))
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun onClick_startWifiPrivacyPage() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                controller.setWifiEntryKey("")
+                controller.Content()
+            }
+        }
+
+        composeTestRule.onNodeWithText(context.getString(R.string.wifi_privacy_settings))
+            .performClick()
+
+        val intent = argumentCaptor<Intent> {
+            verify(context).startActivity(capture())
+        }.firstValue
+        Truth.assertThat(intent.getStringExtra(KEY_DESTINATION))
+            .isEqualTo(WifiPrivacyPageProvider.getRoute(""))
+    }
+
+    private companion object {
+        const val TEST_KEY = "test_key"
+    }
+}
\ No newline at end of file