Merge "InternetPreferenceController V2 (2/n)" into main
diff --git a/src/com/android/settings/network/ConnectivityRepository.kt b/src/com/android/settings/network/ConnectivityRepository.kt
new file mode 100644
index 0000000..3f9b61c
--- /dev/null
+++ b/src/com/android/settings/network/ConnectivityRepository.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+class ConnectivityRepository(context: Context) {
+    private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)!!
+
+    fun networkCapabilitiesFlow(): Flow<NetworkCapabilities> = callbackFlow {
+        val callback = object : NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                networkCapabilities: NetworkCapabilities,
+            ) {
+                trySend(networkCapabilities)
+                Log.d(TAG, "onCapabilitiesChanged: $networkCapabilities")
+            }
+
+            override fun onLost(network: Network) {
+                trySend(NetworkCapabilities())
+                Log.d(TAG, "onLost")
+            }
+        }
+        trySend(getNetworkCapabilities())
+        connectivityManager.registerDefaultNetworkCallback(callback)
+
+        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+    }.conflate().flowOn(Dispatchers.Default)
+
+    private fun getNetworkCapabilities(): NetworkCapabilities =
+        connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+            ?: NetworkCapabilities()
+
+    private companion object {
+        private const val TAG = "ConnectivityRepository"
+    }
+}
diff --git a/src/com/android/settings/network/InternetPreferenceControllerV2.kt b/src/com/android/settings/network/InternetPreferenceControllerV2.kt
index f9d5618..351aca8 100644
--- a/src/com/android/settings/network/InternetPreferenceControllerV2.kt
+++ b/src/com/android/settings/network/InternetPreferenceControllerV2.kt
@@ -22,7 +22,6 @@
 import androidx.preference.PreferenceScreen
 import com.android.settings.R
 import com.android.settings.core.BasePreferenceController
-import com.android.settings.wifi.WifiSummaryRepository
 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
 
 class InternetPreferenceControllerV2(context: Context, preferenceKey: String) :
@@ -40,7 +39,7 @@
     }
 
     override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
-        WifiSummaryRepository(mContext).summaryFlow()
+        InternetPreferenceRepository(mContext).summaryFlow()
             .collectLatestWithLifecycle(viewLifecycleOwner) {
                 preference?.summary = it
             }
diff --git a/src/com/android/settings/network/InternetPreferenceRepository.kt b/src/com/android/settings/network/InternetPreferenceRepository.kt
new file mode 100644
index 0000000..30a98d7
--- /dev/null
+++ b/src/com/android/settings/network/InternetPreferenceRepository.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.wifi.WifiManager
+import android.provider.Settings
+import android.util.Log
+import com.android.settings.R
+import com.android.settings.wifi.WifiSummaryRepository
+import com.android.settings.wifi.repository.WifiRepository
+import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onEach
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class InternetPreferenceRepository(
+    private val context: Context,
+    private val connectivityRepository: ConnectivityRepository = ConnectivityRepository(context),
+    private val wifiSummaryRepository: WifiSummaryRepository = WifiSummaryRepository(context),
+    private val wifiRepository: WifiRepository = WifiRepository(context),
+    private val airplaneModeOnFlow: Flow<Boolean> =
+        context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON),
+) {
+
+    fun summaryFlow(): Flow<String> = connectivityRepository.networkCapabilitiesFlow()
+        .flatMapLatest { capabilities -> capabilities.summaryFlow() }
+        .onEach { Log.d(TAG, "summaryFlow: $it") }
+        .conflate()
+        .flowOn(Dispatchers.Default)
+
+    private fun NetworkCapabilities.summaryFlow(): Flow<String> {
+        if (hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
+            hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+        ) {
+            for (transportType in transportTypes) {
+                if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
+                    return wifiSummaryRepository.summaryFlow()
+                }
+            }
+        }
+        return defaultSummaryFlow()
+    }
+
+    private fun defaultSummaryFlow(): Flow<String> = combine(
+        airplaneModeOnFlow,
+        wifiRepository.wifiStateFlow(),
+    ) { airplaneModeOn: Boolean, wifiState: Int ->
+        context.getString(
+            if (airplaneModeOn && wifiState != WifiManager.WIFI_STATE_ENABLED) {
+                R.string.condition_airplane_title
+            } else {
+                R.string.networks_available
+            }
+        )
+    }
+
+    private companion object {
+        private const val TAG = "InternetPreferenceRepo"
+    }
+}
diff --git a/src/com/android/settings/wifi/repository/WifiRepository.kt b/src/com/android/settings/wifi/repository/WifiRepository.kt
new file mode 100644
index 0000000..77f0b1b
--- /dev/null
+++ b/src/com/android/settings/wifi/repository/WifiRepository.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.repository
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.wifi.WifiManager
+import android.util.Log
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+class WifiRepository(
+    private val context: Context,
+    private val wifiStateChangedActionFlow: Flow<Intent> =
+        context.broadcastReceiverFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)),
+) {
+
+    fun wifiStateFlow() = wifiStateChangedActionFlow
+        .map { intent ->
+            intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN)
+        }
+        .onEach { Log.d(TAG, "wifiStateFlow: $it") }
+
+    private companion object {
+        private const val TAG = "WifiRepository"
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt
new file mode 100644
index 0000000..170b84d
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityRepositoryTest {
+
+    private var networkCallback: NetworkCallback? = null
+
+    private val mockConnectivityManager = mock<ConnectivityManager> {
+        on { registerDefaultNetworkCallback(any()) } doAnswer {
+            networkCallback = it.arguments[0] as NetworkCallback
+        }
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { getSystemService(ConnectivityManager::class.java) } doReturn mockConnectivityManager
+    }
+
+    private val connectivityRepository = ConnectivityRepository(context)
+
+    @Test
+    fun networkCapabilitiesFlow_activeNetworkIsNull_noCrash() = runBlocking {
+        mockConnectivityManager.stub {
+            on { activeNetwork } doReturn null
+            on { getNetworkCapabilities(null) } doReturn null
+        }
+
+        val networkCapabilities =
+            connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
+
+        assertThat(networkCapabilities.transportTypes).isEmpty()
+    }
+
+    @Test
+    fun networkCapabilitiesFlow_getInitialValue() = runBlocking {
+        val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+        }.build()
+        mockConnectivityManager.stub {
+            on { getNetworkCapabilities(null) } doReturn expectedNetworkCapabilities
+        }
+
+        val actualNetworkCapabilities =
+            connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
+
+        assertThat(actualNetworkCapabilities).isSameInstanceAs(expectedNetworkCapabilities)
+    }
+
+    @Test
+    fun networkCapabilitiesFlow_getUpdatedValue() = runBlocking {
+        val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+        }.build()
+
+        val deferredList = async {
+            connectivityRepository.networkCapabilitiesFlow().toListWithTimeout()
+        }
+        delay(100)
+        networkCallback?.onCapabilitiesChanged(mock<Network>(), expectedNetworkCapabilities)
+
+        assertThat(deferredList.await().last()).isSameInstanceAs(expectedNetworkCapabilities)
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt
new file mode 100644
index 0000000..4cd65e7
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.wifi.WifiManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.wifi.WifiSummaryRepository
+import com.android.settings.wifi.repository.WifiRepository
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+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 InternetPreferenceRepositoryTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private val mockConnectivityRepository = mock<ConnectivityRepository>()
+    private val mockWifiSummaryRepository = mock<WifiSummaryRepository>()
+    private val mockWifiRepository = mock<WifiRepository>()
+    private val airplaneModeOnFlow = MutableStateFlow(false)
+
+    private val repository = InternetPreferenceRepository(
+        context = context,
+        connectivityRepository = mockConnectivityRepository,
+        wifiSummaryRepository = mockWifiSummaryRepository,
+        wifiRepository = mockWifiRepository,
+        airplaneModeOnFlow = airplaneModeOnFlow,
+    )
+
+    @Test
+    fun summaryFlow_wifi() = runBlocking {
+        val wifiNetworkCapabilities = NetworkCapabilities.Builder().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+        }.build()
+        mockConnectivityRepository.stub {
+            on { networkCapabilitiesFlow() } doReturn flowOf(wifiNetworkCapabilities)
+        }
+        mockWifiSummaryRepository.stub {
+            on { summaryFlow() } doReturn flowOf(SUMMARY)
+        }
+
+        val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+        assertThat(summary).isEqualTo(SUMMARY)
+    }
+
+    @Test
+    fun summaryFlow_airplaneModeOnAndWifiOn() = runBlocking {
+        mockConnectivityRepository.stub {
+            on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+        }
+        airplaneModeOnFlow.value = true
+        mockWifiRepository.stub {
+            on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_ENABLED)
+        }
+
+        val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+        assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
+    }
+
+    @Test
+    fun summaryFlow_airplaneModeOnAndWifiOff() = runBlocking {
+        mockConnectivityRepository.stub {
+            on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+        }
+        airplaneModeOnFlow.value = true
+        mockWifiRepository.stub {
+            on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
+        }
+
+        val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+        assertThat(summary).isEqualTo(context.getString(R.string.condition_airplane_title))
+    }
+
+    @Test
+    fun summaryFlow_airplaneModeOff() = runBlocking {
+        mockConnectivityRepository.stub {
+            on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+        }
+        airplaneModeOnFlow.value = false
+        mockWifiRepository.stub {
+            on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
+        }
+
+        val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+        assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
+    }
+
+    private companion object {
+        const val SUMMARY = "Summary"
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt
new file mode 100644
index 0000000..dae3617
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.repository
+
+import android.content.Context
+import android.content.Intent
+import android.net.wifi.WifiManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WifiRepositoryTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private val mockWifiStateChangedActionFlow = flowOf(Intent().apply {
+        putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED)
+    })
+
+    private val repository = WifiRepository(context, mockWifiStateChangedActionFlow)
+
+    @Test
+    fun wifiStateFlow() = runBlocking {
+        val wifiState = repository.wifiStateFlow().firstWithTimeoutOrNull()
+
+        assertThat(wifiState).isEqualTo(WifiManager.WIFI_STATE_ENABLED)
+    }
+}