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
This commit is contained in:
Chaohui Wang
2024-06-13 15:21:10 +08:00
parent 8dd270c0ea
commit b0acf0daf5
6 changed files with 386 additions and 50 deletions

View File

@@ -17,44 +17,45 @@
package com.android.settings.wifi package com.android.settings.wifi
import android.content.Context 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.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.R
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
import com.android.settingslib.wifi.WifiStatusTracker import com.android.settingslib.wifi.WifiStatusTracker
import com.android.wifitrackerlib.HotspotNetworkEntry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map 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( class WifiSummaryRepository(
private val context: Context, private val context: Context,
private val wifiStatusTrackerFactory: (callback: Runnable) -> WifiStatusTracker = { callback -> private val wifiStatusRepository: WifiStatusRepository = WifiStatusRepository(context),
WifiStatusTracker( private val wifiPickerRepository: WifiPickerRepository? =
context, if (SharedConnectivityRepository.isDeviceConfigEnabled()) WifiPickerRepository(context)
context.getSystemService(WifiManager::class.java), else null,
context.getSystemService(NetworkScoreManager::class.java),
context.getSystemService(ConnectivityManager::class.java),
callback,
)
},
) { ) {
fun summaryFlow() = wifiStatusTrackerFlow() fun summaryFlow(): Flow<String> {
.map { wifiStatusTracker -> wifiStatusTracker.getSummary() } if (wifiPickerRepository == null) return wifiStatusSummaryFlow()
.conflate() return combine(
.flowOn(Dispatchers.Default) 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 { private fun WifiStatusTracker.getSummary(): String {
if (!enabled) return context.getString(com.android.settings.R.string.switch_off_text) if (!enabled) return context.getString(com.android.settings.R.string.switch_off_text)
@@ -62,30 +63,9 @@ class WifiSummaryRepository(
val sanitizedSsid = WifiInfo.sanitizeSsid(ssid) ?: "" val sanitizedSsid = WifiInfo.sanitizeSsid(ssid) ?: ""
if (statusLabel.isNullOrEmpty()) return sanitizedSsid if (statusLabel.isNullOrEmpty()) return sanitizedSsid
return context.getString( return context.getString(
R.string.preference_summary_default_combination, sanitizedSsid, statusLabel R.string.preference_summary_default_combination,
sanitizedSsid,
statusLabel,
) )
} }
private fun wifiStatusTrackerFlow(): Flow<WifiStatusTracker> = 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)
}
}
} }

View File

@@ -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<WifiEntry?> =
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
}
}

View File

@@ -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<WifiStatusTracker> =
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)
}
}
}

View File

@@ -20,13 +20,19 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R 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.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.wifi.WifiStatusTracker import com.android.settingslib.wifi.WifiStatusTracker
import com.android.wifitrackerlib.HotspotNetworkEntry
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class WifiSummaryRepositoryTest { class WifiSummaryRepositoryTest {
@@ -35,11 +41,22 @@ class WifiSummaryRepositoryTest {
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
private val repository = WifiSummaryRepository(context) { mockWifiStatusTracker } private val mockWifiStatusRepository =
mock<WifiStatusRepository> {
on { wifiStatusTrackerFlow() } doReturn flowOf(mockWifiStatusTracker)
}
private val mockWifiPickerRepository = mock<WifiPickerRepository>()
@Test @Test
fun summaryFlow_wifiDisabled_returnOff() = runBlocking { fun summaryFlow_wifiDisabled_returnOff() = runBlocking {
mockWifiStatusTracker.enabled = false mockWifiStatusTracker.enabled = false
val repository =
WifiSummaryRepository(
context = context,
wifiStatusRepository = mockWifiStatusRepository,
wifiPickerRepository = null,
)
val summary = repository.summaryFlow().firstWithTimeoutOrNull() val summary = repository.summaryFlow().firstWithTimeoutOrNull()
@@ -52,6 +69,12 @@ class WifiSummaryRepositoryTest {
enabled = true enabled = true
connected = false connected = false
} }
val repository =
WifiSummaryRepository(
context = context,
wifiStatusRepository = mockWifiStatusRepository,
wifiPickerRepository = null,
)
val summary = repository.summaryFlow().firstWithTimeoutOrNull() val summary = repository.summaryFlow().firstWithTimeoutOrNull()
@@ -65,6 +88,12 @@ class WifiSummaryRepositoryTest {
connected = true connected = true
ssid = TEST_SSID ssid = TEST_SSID
} }
val repository =
WifiSummaryRepository(
context = context,
wifiStatusRepository = mockWifiStatusRepository,
wifiPickerRepository = null,
)
val summary = repository.summaryFlow().firstWithTimeoutOrNull() val summary = repository.summaryFlow().firstWithTimeoutOrNull()
@@ -79,14 +108,40 @@ class WifiSummaryRepositoryTest {
ssid = TEST_SSID ssid = TEST_SSID
statusLabel = STATUS_LABEL statusLabel = STATUS_LABEL
} }
val repository =
WifiSummaryRepository(
context = context,
wifiStatusRepository = mockWifiStatusRepository,
wifiPickerRepository = null,
)
val summary = repository.summaryFlow().firstWithTimeoutOrNull() val summary = repository.summaryFlow().firstWithTimeoutOrNull()
assertThat(summary).isEqualTo("$TEST_SSID / $STATUS_LABEL") assertThat(summary).isEqualTo("$TEST_SSID / $STATUS_LABEL")
} }
@Test
fun summaryFlow_withWifiPickerRepository() = runBlocking {
val hotspotNetworkEntry =
mock<HotspotNetworkEntry> { 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 { private companion object {
const val TEST_SSID = "Test Ssid" const val TEST_SSID = "Test Ssid"
const val STATUS_LABEL = "Very Fast" const val STATUS_LABEL = "Very Fast"
const val ALTERNATE_SUMMARY = "Alternate Summary"
} }
} }

View File

@@ -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<WifiPickerTracker>()
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<WifiEntry>() }
callback?.onWifiEntriesChanged()
assertThat(listDeferred.await().filterNotNull()).isNotEmpty()
}
}

View File

@@ -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<WifiStatusTracker>()
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)
}
}