InternetPreferenceController V2 (3/n)
Add DataSubscriptionRepository, when cellular connection, show the active subscription name. Bug: 339884322 Flag: com.android.settings.flags.internet_preference_controller_v2 Test: manual - on Internet Test: unit test Change-Id: If2a3e7f8df1b1ed89bc760ec5165182b3e9b64a8
This commit is contained in:
@@ -22,6 +22,7 @@ import android.net.wifi.WifiManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.android.settings.R
|
||||
import com.android.settings.network.telephony.DataSubscriptionRepository
|
||||
import com.android.settings.wifi.WifiSummaryRepository
|
||||
import com.android.settings.wifi.repository.WifiRepository
|
||||
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
|
||||
@@ -39,31 +40,39 @@ class InternetPreferenceRepository(
|
||||
private val context: Context,
|
||||
private val connectivityRepository: ConnectivityRepository = ConnectivityRepository(context),
|
||||
private val wifiSummaryRepository: WifiSummaryRepository = WifiSummaryRepository(context),
|
||||
private val dataSubscriptionRepository: DataSubscriptionRepository =
|
||||
DataSubscriptionRepository(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()
|
||||
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) &&
|
||||
if (
|
||||
hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
) {
|
||||
for (transportType in transportTypes) {
|
||||
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
return wifiSummaryRepository.summaryFlow()
|
||||
when (transportType) {
|
||||
NetworkCapabilities.TRANSPORT_WIFI -> return wifiSummaryRepository.summaryFlow()
|
||||
NetworkCapabilities.TRANSPORT_CELLULAR ->
|
||||
return dataSubscriptionRepository.dataSummaryFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultSummaryFlow()
|
||||
}
|
||||
|
||||
private fun defaultSummaryFlow(): Flow<String> = combine(
|
||||
private fun defaultSummaryFlow(): Flow<String> =
|
||||
combine(
|
||||
airplaneModeOnFlow,
|
||||
wifiRepository.wifiStateFlow(),
|
||||
) { airplaneModeOn: Boolean, wifiState: Int ->
|
||||
|
@@ -408,7 +408,6 @@ public class SubscriptionUtil {
|
||||
*
|
||||
* @return map of active subscription ids to display names.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static CharSequence getUniqueSubscriptionDisplayName(
|
||||
Integer subscriptionId, Context context) {
|
||||
final Map<Integer, CharSequence> displayNames = getUniqueSubscriptionDisplayNames(context);
|
||||
|
@@ -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.network.telephony
|
||||
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.settings.R
|
||||
import com.android.settings.network.SubscriptionUtil
|
||||
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
class DataSubscriptionRepository(
|
||||
private val context: Context,
|
||||
private val getDisplayName: (subId: Int) -> String = { subId ->
|
||||
SubscriptionUtil.getUniqueSubscriptionDisplayName(subId, context).toString()
|
||||
},
|
||||
) {
|
||||
private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
|
||||
private val subscriptionManager = context.requireSubscriptionManager()
|
||||
|
||||
fun defaultDataSubscriptionIdFlow(): Flow<Int> =
|
||||
context
|
||||
.broadcastReceiverFlow(
|
||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
)
|
||||
.map { it.getIntExtra(SUBSCRIPTION_KEY, SubscriptionManager.INVALID_SUBSCRIPTION_ID) }
|
||||
.onStart { emit(SubscriptionManager.getDefaultDataSubscriptionId()) }
|
||||
.conflate()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
fun activeDataSubscriptionIdFlow(): Flow<Int> =
|
||||
telephonyManager.telephonyCallbackFlow {
|
||||
object : TelephonyCallback(), TelephonyCallback.ActiveDataSubscriptionIdListener {
|
||||
override fun onActiveDataSubscriptionIdChanged(subId: Int) {
|
||||
trySend(subId)
|
||||
Log.d(TAG, "activeDataSubscriptionIdFlow: $subId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dataSummaryFlow(): Flow<String> =
|
||||
combine(defaultDataSubscriptionIdFlow(), activeDataSubscriptionIdFlow()) {
|
||||
defaultSubId,
|
||||
activeSubId ->
|
||||
DataSubscriptionIds(defaultSubId, activeSubId)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { it.getDataSummary() }
|
||||
.conflate()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private data class DataSubscriptionIds(
|
||||
val defaultSubId: Int,
|
||||
val activeSubId: Int,
|
||||
)
|
||||
|
||||
private fun DataSubscriptionIds.getDataSummary(): String {
|
||||
val activeSubInfo = subscriptionManager.getActiveSubscriptionInfo(activeSubId) ?: return ""
|
||||
if (!SubscriptionUtil.isSubscriptionVisible(subscriptionManager, context, activeSubInfo)) {
|
||||
return getDisplayName(defaultSubId)
|
||||
}
|
||||
val uniqueName = getDisplayName(activeSubId)
|
||||
return if (activeSubId == defaultSubId) {
|
||||
uniqueName
|
||||
} else {
|
||||
context.getString(R.string.mobile_data_temp_using, uniqueName)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DataSubscriptionRepo"
|
||||
|
||||
@VisibleForTesting const val SUBSCRIPTION_KEY = "subscription"
|
||||
}
|
||||
}
|
@@ -114,14 +114,17 @@ class TelephonyRepository(
|
||||
fun <T> Context.telephonyCallbackFlow(
|
||||
subId: Int,
|
||||
block: ProducerScope<T>.() -> TelephonyCallback,
|
||||
): Flow<T> = callbackFlow {
|
||||
val telephonyManager = telephonyManager(subId)
|
||||
): Flow<T> = telephonyManager(subId).telephonyCallbackFlow(block)
|
||||
|
||||
/** Creates an instance of a cold Flow for Telephony callback. */
|
||||
fun <T> TelephonyManager.telephonyCallbackFlow(
|
||||
block: ProducerScope<T>.() -> TelephonyCallback,
|
||||
): Flow<T> = callbackFlow {
|
||||
val callback = block()
|
||||
|
||||
telephonyManager.registerTelephonyCallback(Dispatchers.Default.asExecutor(), callback)
|
||||
registerTelephonyCallback(Dispatchers.Default.asExecutor(), callback)
|
||||
|
||||
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
|
||||
awaitClose { unregisterTelephonyCallback(callback) }
|
||||
}.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
fun Context.telephonyManager(subId: Int): TelephonyManager =
|
||||
|
@@ -46,6 +46,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.settings.R
|
||||
import com.android.settings.network.SubscriptionInfoListViewModel
|
||||
import com.android.settings.network.telephony.DataSubscriptionRepository
|
||||
import com.android.settings.network.telephony.TelephonyRepository
|
||||
import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo
|
||||
import com.android.settings.wifi.WifiPickerTrackerHelper
|
||||
@@ -158,7 +159,7 @@ open class NetworkCellularGroupProvider : SettingsPageProvider {
|
||||
selectableSubscriptionInfoListFlow,
|
||||
context.defaultVoiceSubscriptionFlow(),
|
||||
context.defaultSmsSubscriptionFlow(),
|
||||
context.defaultDefaultDataSubscriptionFlow(),
|
||||
DataSubscriptionRepository(context).defaultDataSubscriptionIdFlow(),
|
||||
this::refreshUiStates,
|
||||
).flowOn(Dispatchers.Default)
|
||||
|
||||
@@ -370,15 +371,6 @@ private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
|
||||
).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
|
||||
.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
private fun Context.defaultDefaultDataSubscriptionFlow(): Flow<Int> =
|
||||
merge(
|
||||
flowOf(null), // kick an initial value
|
||||
broadcastReceiverFlow(
|
||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
),
|
||||
).map { SubscriptionManager.getDefaultDataSubscriptionId() }
|
||||
.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
suspend fun setDefaultVoice(
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
subId: Int
|
||||
|
@@ -22,6 +22,7 @@ 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.network.telephony.DataSubscriptionRepository
|
||||
import com.android.settings.wifi.WifiSummaryRepository
|
||||
import com.android.settings.wifi.repository.WifiRepository
|
||||
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
|
||||
@@ -42,31 +43,51 @@ class InternetPreferenceRepositoryTest {
|
||||
|
||||
private val mockConnectivityRepository = mock<ConnectivityRepository>()
|
||||
private val mockWifiSummaryRepository = mock<WifiSummaryRepository>()
|
||||
private val mockDataSubscriptionRepository = mock<DataSubscriptionRepository>()
|
||||
private val mockWifiRepository = mock<WifiRepository>()
|
||||
private val airplaneModeOnFlow = MutableStateFlow(false)
|
||||
|
||||
private val repository = InternetPreferenceRepository(
|
||||
private val repository =
|
||||
InternetPreferenceRepository(
|
||||
context = context,
|
||||
connectivityRepository = mockConnectivityRepository,
|
||||
wifiSummaryRepository = mockWifiSummaryRepository,
|
||||
dataSubscriptionRepository = mockDataSubscriptionRepository,
|
||||
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()
|
||||
val wifiNetworkCapabilities =
|
||||
NetworkCapabilities.Builder()
|
||||
.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)
|
||||
mockWifiSummaryRepository.stub { on { summaryFlow() } doReturn flowOf(SUMMARY) }
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(SUMMARY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun summaryFlow_cellular() = runBlocking {
|
||||
val wifiNetworkCapabilities =
|
||||
NetworkCapabilities.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
mockConnectivityRepository.stub {
|
||||
on { networkCapabilitiesFlow() } doReturn flowOf(wifiNetworkCapabilities)
|
||||
}
|
||||
mockDataSubscriptionRepository.stub { on { dataSummaryFlow() } doReturn flowOf(SUMMARY) }
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(SUMMARY)
|
||||
|
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.telephony
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyCallback
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settings.network.telephony.DataSubscriptionRepository.Companion.SUBSCRIPTION_KEY
|
||||
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.argThat
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DataSubscriptionRepositoryTest {
|
||||
|
||||
private var activeDataSubIdListener: TelephonyCallback.ActiveDataSubscriptionIdListener? = null
|
||||
|
||||
private val mockTelephonyManager =
|
||||
mock<TelephonyManager> {
|
||||
on { registerTelephonyCallback(any(), any()) } doAnswer
|
||||
{
|
||||
activeDataSubIdListener =
|
||||
it.arguments[1] as TelephonyCallback.ActiveDataSubscriptionIdListener
|
||||
}
|
||||
}
|
||||
|
||||
private val mockSubscriptionManager =
|
||||
mock<SubscriptionManager> {
|
||||
on { getActiveSubscriptionInfo(SUB_ID_10) } doReturn
|
||||
SubscriptionInfo.Builder().setId(SUB_ID_10).build()
|
||||
on { getActiveSubscriptionInfo(SUB_ID_20) } doReturn
|
||||
SubscriptionInfo.Builder().setId(SUB_ID_20).build()
|
||||
}
|
||||
|
||||
private val context: Context =
|
||||
spy(ApplicationProvider.getApplicationContext()) {
|
||||
on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager
|
||||
on { getSystemService(SubscriptionManager::class.java) } doReturn
|
||||
mockSubscriptionManager
|
||||
|
||||
doAnswer {
|
||||
val broadcastReceiver = it.arguments[0] as BroadcastReceiver
|
||||
val intent = Intent().apply { putExtra(SUBSCRIPTION_KEY, SUB_ID_10) }
|
||||
broadcastReceiver.onReceive(mock, intent)
|
||||
null
|
||||
}
|
||||
.whenever(mock)
|
||||
.registerReceiver(
|
||||
any(),
|
||||
argThat {
|
||||
hasAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
},
|
||||
any(),
|
||||
)
|
||||
}
|
||||
|
||||
private val repository = DataSubscriptionRepository(context) { subId -> "Name$subId" }
|
||||
|
||||
@Test
|
||||
fun defaultDataSubscriptionIdFlow() = runBlocking {
|
||||
val defaultSubIdDeferred = async {
|
||||
repository.defaultDataSubscriptionIdFlow().toListWithTimeout()
|
||||
}
|
||||
delay(100)
|
||||
|
||||
assertThat(defaultSubIdDeferred.await()).contains(SUB_ID_10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeDataSubscriptionIdFlow() = runBlocking {
|
||||
val activeSubIdDeferred = async {
|
||||
repository.activeDataSubscriptionIdFlow().toListWithTimeout()
|
||||
}
|
||||
delay(100)
|
||||
|
||||
activeDataSubIdListener?.onActiveDataSubscriptionIdChanged(SUB_ID_20)
|
||||
|
||||
assertThat(activeSubIdDeferred.await()).contains(SUB_ID_20)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataSummaryFlow_defaultIsActive() = runBlocking {
|
||||
val summaryDeferred = async { repository.dataSummaryFlow().firstWithTimeoutOrNull() }
|
||||
delay(100)
|
||||
|
||||
activeDataSubIdListener?.onActiveDataSubscriptionIdChanged(SUB_ID_10)
|
||||
|
||||
assertThat(summaryDeferred.await()).isEqualTo("Name10")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dataSummaryFlow_defaultIsNotActive() = runBlocking {
|
||||
val summaryDeferred = async { repository.dataSummaryFlow().firstWithTimeoutOrNull() }
|
||||
delay(100)
|
||||
|
||||
activeDataSubIdListener?.onActiveDataSubscriptionIdChanged(SUB_ID_20)
|
||||
|
||||
assertThat(summaryDeferred.await()).isEqualTo("Temporarily using Name20")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SUB_ID_10 = 10
|
||||
const val SUB_ID_20 = 20
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user