diff --git a/src/com/android/settings/network/ConnectivityRepository.kt b/src/com/android/settings/network/ConnectivityRepository.kt new file mode 100644 index 00000000000..3f9b61c394d --- /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 = 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 f9d56189476..351aca83526 100644 --- a/src/com/android/settings/network/InternetPreferenceControllerV2.kt +++ b/src/com/android/settings/network/InternetPreferenceControllerV2.kt @@ -22,7 +22,6 @@ import androidx.preference.Preference 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 @@ class InternetPreferenceControllerV2(context: Context, preferenceKey: String) : } 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 00000000000..30a98d7cb1d --- /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 = + context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON), +) { + + fun summaryFlow(): Flow = connectivityRepository.networkCapabilitiesFlow() + .flatMapLatest { capabilities -> capabilities.summaryFlow() } + .onEach { Log.d(TAG, "summaryFlow: $it") } + .conflate() + .flowOn(Dispatchers.Default) + + private fun NetworkCapabilities.summaryFlow(): Flow { + 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 = 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 00000000000..77f0b1b47cf --- /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 = + 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 00000000000..170b84d8884 --- /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 { + 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(), 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 00000000000..4cd65e74505 --- /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() + private val mockWifiSummaryRepository = mock() + private val mockWifiRepository = mock() + 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 00000000000..dae3617c37e --- /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) + } +}