From b0acf0daf5f9d86ef09c6b9ff60aff99e09664d3 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 13 Jun 2024 15:21:10 +0800 Subject: [PATCH] InternetPreferenceController V2 (7/7) Support alternateSummary for HotspotNetworkEntry. Bug: 339884322 Flag: com.android.settings.flags.internet_preference_controller_v2 Test: manual - on Internet Test: unit test Change-Id: I6a454e86453ea8ed597a032d3f5769b6cf9102ba --- .../settings/wifi/WifiSummaryRepository.kt | 78 +++++--------- .../wifi/repository/WifiPickerRepository.kt | 101 ++++++++++++++++++ .../wifi/repository/WifiStatusRepository.kt | 75 +++++++++++++ .../wifi/WifiSummaryRepositoryTest.kt | 57 +++++++++- .../repository/WifiPickerRepositoryTest.kt | 78 ++++++++++++++ .../repository/WifiStatusRepositoryTest.kt | 47 ++++++++ 6 files changed, 386 insertions(+), 50 deletions(-) create mode 100644 src/com/android/settings/wifi/repository/WifiPickerRepository.kt create mode 100644 src/com/android/settings/wifi/repository/WifiStatusRepository.kt create mode 100644 tests/spa_unit/src/com/android/settings/wifi/repository/WifiPickerRepositoryTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/wifi/repository/WifiStatusRepositoryTest.kt diff --git a/src/com/android/settings/wifi/WifiSummaryRepository.kt b/src/com/android/settings/wifi/WifiSummaryRepository.kt index d81ae9a873a..6e34cf5ba5d 100644 --- a/src/com/android/settings/wifi/WifiSummaryRepository.kt +++ b/src/com/android/settings/wifi/WifiSummaryRepository.kt @@ -17,44 +17,45 @@ package com.android.settings.wifi import android.content.Context -import android.content.IntentFilter -import android.net.ConnectivityManager -import android.net.NetworkScoreManager import android.net.wifi.WifiInfo -import android.net.wifi.WifiManager +import com.android.settings.wifi.repository.SharedConnectivityRepository +import com.android.settings.wifi.repository.WifiPickerRepository +import com.android.settings.wifi.repository.WifiStatusRepository import com.android.settingslib.R -import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow import com.android.settingslib.wifi.WifiStatusTracker +import com.android.wifitrackerlib.HotspotNetworkEntry import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -/** - * Repository that listeners to wifi callback and provide wifi summary flow to client. - */ +/** Repository that listeners to wifi callback and provide wifi summary flow to client. */ class WifiSummaryRepository( private val context: Context, - private val wifiStatusTrackerFactory: (callback: Runnable) -> WifiStatusTracker = { callback -> - WifiStatusTracker( - context, - context.getSystemService(WifiManager::class.java), - context.getSystemService(NetworkScoreManager::class.java), - context.getSystemService(ConnectivityManager::class.java), - callback, - ) - }, + private val wifiStatusRepository: WifiStatusRepository = WifiStatusRepository(context), + private val wifiPickerRepository: WifiPickerRepository? = + if (SharedConnectivityRepository.isDeviceConfigEnabled()) WifiPickerRepository(context) + else null, ) { - fun summaryFlow() = wifiStatusTrackerFlow() - .map { wifiStatusTracker -> wifiStatusTracker.getSummary() } - .conflate() - .flowOn(Dispatchers.Default) + fun summaryFlow(): Flow { + if (wifiPickerRepository == null) return wifiStatusSummaryFlow() + return combine( + wifiStatusSummaryFlow(), + wifiPickerRepository.connectedWifiEntryFlow(), + ) { wifiStatusSummary, wifiEntry -> + if (wifiEntry is HotspotNetworkEntry) wifiEntry.alternateSummary else wifiStatusSummary + } + } + + private fun wifiStatusSummaryFlow() = + wifiStatusRepository + .wifiStatusTrackerFlow() + .map { wifiStatusTracker -> wifiStatusTracker.getSummary() } + .conflate() + .flowOn(Dispatchers.Default) private fun WifiStatusTracker.getSummary(): String { if (!enabled) return context.getString(com.android.settings.R.string.switch_off_text) @@ -62,30 +63,9 @@ class WifiSummaryRepository( val sanitizedSsid = WifiInfo.sanitizeSsid(ssid) ?: "" if (statusLabel.isNullOrEmpty()) return sanitizedSsid return context.getString( - R.string.preference_summary_default_combination, sanitizedSsid, statusLabel + R.string.preference_summary_default_combination, + sanitizedSsid, + statusLabel, ) } - - private fun wifiStatusTrackerFlow(): Flow = callbackFlow { - var wifiStatusTracker: WifiStatusTracker? = null - wifiStatusTracker = wifiStatusTrackerFactory { wifiStatusTracker?.let(::trySend) } - - context.broadcastReceiverFlow(INTENT_FILTER) - .onEach { intent -> wifiStatusTracker.handleBroadcast(intent) } - .launchIn(this) - - wifiStatusTracker.setListening(true) - wifiStatusTracker.fetchInitialState() - trySend(wifiStatusTracker) - - awaitClose { wifiStatusTracker.setListening(false) } - }.conflate().flowOn(Dispatchers.Default) - - private companion object { - val INTENT_FILTER = IntentFilter().apply { - addAction(WifiManager.WIFI_STATE_CHANGED_ACTION) - addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION) - addAction(WifiManager.RSSI_CHANGED_ACTION) - } - } } diff --git a/src/com/android/settings/wifi/repository/WifiPickerRepository.kt b/src/com/android/settings/wifi/repository/WifiPickerRepository.kt new file mode 100644 index 00000000000..791fa4b4e2e --- /dev/null +++ b/src/com/android/settings/wifi/repository/WifiPickerRepository.kt @@ -0,0 +1,101 @@ +/* + * 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.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Process +import android.os.SystemClock +import android.util.Log +import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.wifitrackerlib.WifiEntry +import com.android.wifitrackerlib.WifiPickerTracker +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 +import kotlinx.coroutines.flow.onEach + +/** Repository that listeners to wifi picker callback and provide wifi picker flow to client. */ +class WifiPickerRepository( + private val context: Context, + private val createWifiPickerTracker: + ( + workerThread: HandlerThread, callback: WifiPickerTracker.WifiPickerTrackerCallback + ) -> WifiPickerTracker = + { workerThread, callback -> + featureFactory.wifiTrackerLibProvider.createWifiPickerTracker( + null, + context, + Handler(Looper.getMainLooper()), + workerThread.getThreadHandler(), + SystemClock.elapsedRealtimeClock(), + MAX_SCAN_AGE_MILLIS, + SCAN_INTERVAL_MILLIS, + callback, + ) + } +) { + + fun connectedWifiEntryFlow(): Flow = + callbackFlow { + val workerThread = + HandlerThread( + /* name = */ "$TAG{${Integer.toHexString(System.identityHashCode(this))}}", + /* priority = */ Process.THREAD_PRIORITY_BACKGROUND, + ) + workerThread.start() + var tracker: WifiPickerTracker? = null + val callback = + object : WifiPickerTracker.WifiPickerTrackerCallback { + override fun onWifiEntriesChanged() { + trySend(tracker?.connectedWifiEntry) + } + + override fun onWifiStateChanged() {} + + override fun onNumSavedNetworksChanged() {} + + override fun onNumSavedSubscriptionsChanged() {} + } + + tracker = createWifiPickerTracker(workerThread, callback) + tracker.onStart() + + awaitClose { + tracker.onStop() + tracker.onDestroy() + workerThread.quit() + } + } + .conflate() + .onEach { Log.d(TAG, "connectedWifiEntryFlow: $it") } + .flowOn(Dispatchers.Default) + + companion object { + private const val TAG = "WifiPickerRepository" + + /** Max age of tracked WifiEntries */ + private const val MAX_SCAN_AGE_MILLIS: Long = 15000 + /** Interval between initiating WifiPickerTracker scans */ + private const val SCAN_INTERVAL_MILLIS: Long = 10000 + } +} diff --git a/src/com/android/settings/wifi/repository/WifiStatusRepository.kt b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt new file mode 100644 index 00000000000..f97ed492507 --- /dev/null +++ b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt @@ -0,0 +1,75 @@ +/* + * 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.IntentFilter +import android.net.ConnectivityManager +import android.net.NetworkScoreManager +import android.net.wifi.WifiManager +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow +import com.android.settingslib.wifi.WifiStatusTracker +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 +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** Repository that listeners to wifi callback and provide wifi status flow to client. */ +class WifiStatusRepository( + private val context: Context, + private val wifiStatusTrackerFactory: (callback: Runnable) -> WifiStatusTracker = { callback -> + WifiStatusTracker( + context, + context.getSystemService(WifiManager::class.java), + context.getSystemService(NetworkScoreManager::class.java), + context.getSystemService(ConnectivityManager::class.java), + callback, + ) + }, +) { + fun wifiStatusTrackerFlow(): Flow = + callbackFlow { + var wifiStatusTracker: WifiStatusTracker? = null + wifiStatusTracker = wifiStatusTrackerFactory { wifiStatusTracker?.let(::trySend) } + + context + .broadcastReceiverFlow(INTENT_FILTER) + .onEach { intent -> wifiStatusTracker.handleBroadcast(intent) } + .launchIn(this) + + wifiStatusTracker.setListening(true) + wifiStatusTracker.fetchInitialState() + trySend(wifiStatusTracker) + + awaitClose { wifiStatusTracker.setListening(false) } + } + .conflate() + .flowOn(Dispatchers.Default) + + private companion object { + val INTENT_FILTER = + IntentFilter().apply { + addAction(WifiManager.WIFI_STATE_CHANGED_ACTION) + addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION) + addAction(WifiManager.RSSI_CHANGED_ACTION) + } + } +} diff --git a/tests/spa_unit/src/com/android/settings/wifi/WifiSummaryRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/WifiSummaryRepositoryTest.kt index 2f22851ba80..d7a92d6f8ea 100644 --- a/tests/spa_unit/src/com/android/settings/wifi/WifiSummaryRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/wifi/WifiSummaryRepositoryTest.kt @@ -20,13 +20,19 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settings.wifi.repository.WifiPickerRepository +import com.android.settings.wifi.repository.WifiStatusRepository import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull import com.android.settingslib.wifi.WifiStatusTracker +import com.android.wifitrackerlib.HotspotNetworkEntry 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 +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.stub @RunWith(AndroidJUnit4::class) class WifiSummaryRepositoryTest { @@ -35,11 +41,22 @@ class WifiSummaryRepositoryTest { private val context: Context = ApplicationProvider.getApplicationContext() - private val repository = WifiSummaryRepository(context) { mockWifiStatusTracker } + private val mockWifiStatusRepository = + mock { + on { wifiStatusTrackerFlow() } doReturn flowOf(mockWifiStatusTracker) + } + + private val mockWifiPickerRepository = mock() @Test fun summaryFlow_wifiDisabled_returnOff() = runBlocking { mockWifiStatusTracker.enabled = false + val repository = + WifiSummaryRepository( + context = context, + wifiStatusRepository = mockWifiStatusRepository, + wifiPickerRepository = null, + ) val summary = repository.summaryFlow().firstWithTimeoutOrNull() @@ -52,6 +69,12 @@ class WifiSummaryRepositoryTest { enabled = true connected = false } + val repository = + WifiSummaryRepository( + context = context, + wifiStatusRepository = mockWifiStatusRepository, + wifiPickerRepository = null, + ) val summary = repository.summaryFlow().firstWithTimeoutOrNull() @@ -65,6 +88,12 @@ class WifiSummaryRepositoryTest { connected = true ssid = TEST_SSID } + val repository = + WifiSummaryRepository( + context = context, + wifiStatusRepository = mockWifiStatusRepository, + wifiPickerRepository = null, + ) val summary = repository.summaryFlow().firstWithTimeoutOrNull() @@ -79,14 +108,40 @@ class WifiSummaryRepositoryTest { ssid = TEST_SSID statusLabel = STATUS_LABEL } + val repository = + WifiSummaryRepository( + context = context, + wifiStatusRepository = mockWifiStatusRepository, + wifiPickerRepository = null, + ) val summary = repository.summaryFlow().firstWithTimeoutOrNull() assertThat(summary).isEqualTo("$TEST_SSID / $STATUS_LABEL") } + @Test + fun summaryFlow_withWifiPickerRepository() = runBlocking { + val hotspotNetworkEntry = + mock { on { alternateSummary } doReturn ALTERNATE_SUMMARY } + mockWifiPickerRepository.stub { + on { connectedWifiEntryFlow() } doReturn flowOf(hotspotNetworkEntry) + } + val repository = + WifiSummaryRepository( + context = context, + wifiStatusRepository = mockWifiStatusRepository, + wifiPickerRepository = mockWifiPickerRepository, + ) + + val summary = repository.summaryFlow().firstWithTimeoutOrNull() + + assertThat(summary).isEqualTo(ALTERNATE_SUMMARY) + } + private companion object { const val TEST_SSID = "Test Ssid" const val STATUS_LABEL = "Very Fast" + const val ALTERNATE_SUMMARY = "Alternate Summary" } } diff --git a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiPickerRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiPickerRepositoryTest.kt new file mode 100644 index 00000000000..cdcc13db33c --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiPickerRepositoryTest.kt @@ -0,0 +1,78 @@ +/* + * 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 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.android.wifitrackerlib.WifiEntry +import com.android.wifitrackerlib.WifiPickerTracker +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.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class WifiPickerRepositoryTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val mockWifiPickerTracker = mock() + + private var callback: WifiPickerTracker.WifiPickerTrackerCallback? = null + + private val repository = + WifiPickerRepository(context) { _, callback -> + this.callback = callback + mockWifiPickerTracker + } + + @Test + fun connectedWifiEntryFlow_callOnStartOnStopAndOnDestroy() = runBlocking { + repository.connectedWifiEntryFlow().firstWithTimeoutOrNull() + + verify(mockWifiPickerTracker).onStart() + verify(mockWifiPickerTracker).onStop() + verify(mockWifiPickerTracker).onDestroy() + } + + @Test + fun connectedWifiEntryFlow_initial() = runBlocking { + val wifiEntry = repository.connectedWifiEntryFlow().firstWithTimeoutOrNull() + + assertThat(wifiEntry).isNull() + } + + @Test + fun connectedWifiEntryFlow_onWifiEntriesChanged() = runBlocking { + val listDeferred = async { repository.connectedWifiEntryFlow().toListWithTimeout() } + delay(100) + + mockWifiPickerTracker.stub { on { connectedWifiEntry } doReturn mock() } + callback?.onWifiEntriesChanged() + + assertThat(listDeferred.await().filterNotNull()).isNotEmpty() + } +} diff --git a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiStatusRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiStatusRepositoryTest.kt new file mode 100644 index 00000000000..200542e6e30 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiStatusRepositoryTest.kt @@ -0,0 +1,47 @@ +/* + * 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 androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.wifi.WifiStatusTracker +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class WifiStatusRepositoryTest { + + private val mockWifiStatusTracker = mock() + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val repository = WifiStatusRepository(context) { mockWifiStatusTracker } + + @Test + fun wifiStatusTrackerFlow() = runBlocking { + mockWifiStatusTracker.enabled = false + + val wifiStatusTracker = repository.wifiStatusTrackerFlow().firstWithTimeoutOrNull() + + assertThat(wifiStatusTracker).isSameInstanceAs(mockWifiStatusTracker) + } +}