Merge "New MobileDataEnabledFlow" into main

This commit is contained in:
Chaohui Wang
2023-11-06 02:56:31 +00:00
committed by Android (Google) Code Review
9 changed files with 157 additions and 252 deletions

View File

@@ -31,9 +31,10 @@ import androidx.preference.Preference
import com.android.settings.R import com.android.settings.R
import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.datausage.lib.NetworkUsageData import com.android.settings.datausage.lib.NetworkUsageData
import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.network.MobileNetworkRepository import com.android.settings.network.MobileNetworkRepository
import com.android.settings.network.mobileDataEnabledFlow
import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.utils.ThreadUtils import com.android.settingslib.utils.ThreadUtils
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@@ -43,10 +44,7 @@ import kotlin.jvm.optionals.getOrNull
* to inspect based on usage cycle and control through [NetworkPolicy]. * to inspect based on usage cycle and control through [NetworkPolicy].
*/ */
@OpenForTesting @OpenForTesting
open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Client { open class DataUsageList : DataUsageBaseFragment() {
@VisibleForTesting
lateinit var dataStateListener: MobileDataEnabledListener
@JvmField @JvmField
@VisibleForTesting @VisibleForTesting
var template: NetworkTemplate? = null var template: NetworkTemplate? = null
@@ -89,7 +87,6 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl
return return
} }
updateSubscriptionInfoEntity() updateSubscriptionInfoEntity()
dataStateListener = MobileDataEnabledListener(activity, this)
dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply { dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply {
init(template) init(template)
} }
@@ -103,6 +100,9 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl
override fun onViewCreated(v: View, savedInstanceState: Bundle?) { override fun onViewCreated(v: View, savedInstanceState: Bundle?) {
super.onViewCreated(v, savedInstanceState) super.onViewCreated(v, savedInstanceState)
requireContext().mobileDataEnabledFlow(subId)
.collectLatestWithLifecycle(viewLifecycleOwner) { updatePolicy() }
val template = template ?: return val template = template ?: return
dataUsageListHeaderController = DataUsageListHeaderController( dataUsageListHeaderController = DataUsageListHeaderController(
setPinnedHeaderView(R.layout.apps_filter_spinner), setPinnedHeaderView(R.layout.apps_filter_spinner),
@@ -114,17 +114,6 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl
) )
} }
override fun onResume() {
super.onResume()
dataStateListener.start(subId)
updatePolicy()
}
override fun onPause() {
super.onPause()
dataStateListener.stop()
}
override fun getPreferenceScreenResId() = R.xml.data_usage_list override fun getPreferenceScreenResId() = R.xml.data_usage_list
override fun getLogTag() = TAG override fun getLogTag() = TAG
@@ -154,13 +143,6 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl
} }
} }
/**
* Implementation of `MobileDataEnabledListener.Client`
*/
override fun onMobileDataEnabledChange() {
updatePolicy()
}
/** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */ /** Update chart sweeps and cycle list to reflect [NetworkPolicy] for current [template]. */
@VisibleForTesting @VisibleForTesting
fun updatePolicy() { fun updatePolicy() {

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 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.provider.Settings
import android.telephony.SubscriptionManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalChangeFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
/**
* Flow for mobile data enabled changed event.
*
* Note: This flow can only notify enabled status changes, cannot provide the latest status.
*/
fun Context.mobileDataEnabledFlow(subId: Int): Flow<Unit> {
val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA)
return when (subId) {
SubscriptionManager.INVALID_SUBSCRIPTION_ID -> flow
else -> {
val subIdFlow = settingsGlobalChangeFlow(
name = Settings.Global.MOBILE_DATA + subId,
sendInitialValue = false,
)
merge(flow, subIdFlow)
}
}
}

View File

@@ -20,7 +20,12 @@ import android.content.Context;
import android.provider.Settings; import android.provider.Settings;
import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager;
/** Helper class to listen for changes in the enabled state of mobile data. */ /**
* Helper class to listen for changes in the enabled state of mobile data.
*
* @deprecated use {@link MobileDataEnabledFlowKt} instead
*/
@Deprecated
public class MobileDataEnabledListener { public class MobileDataEnabledListener {
private Context mContext; private Context mContext;
private Client mClient; private Client mClient;

View File

@@ -28,15 +28,16 @@ import com.android.settings.SettingsPreferenceFragment
import com.android.settings.dashboard.DashboardFragment import com.android.settings.dashboard.DashboardFragment
import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.network.telephony.MobileNetworkUtils
import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.utils.observeSettingsGlobalBoolean
import com.android.settingslib.search.SearchIndexable import com.android.settingslib.search.SearchIndexable
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv()) @SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
class MobileNetworkListFragment : DashboardFragment() { class MobileNetworkListFragment : DashboardFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeAirplaneModeAndFinishIfOn() collectAirplaneModeAndFinishIfOn()
} }
override fun onResume() { override fun onResume() {
@@ -59,11 +60,9 @@ class MobileNetworkListFragment : DashboardFragment() {
private const val KEY_ADD_SIM = "add_sim" private const val KEY_ADD_SIM = "add_sim"
@JvmStatic @JvmStatic
fun SettingsPreferenceFragment.observeAirplaneModeAndFinishIfOn() { fun SettingsPreferenceFragment.collectAirplaneModeAndFinishIfOn() {
requireContext().observeSettingsGlobalBoolean( requireContext().settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON)
name = Settings.Global.AIRPLANE_MODE_ON, .collectLatestWithLifecycle(viewLifecycleOwner) { isAirplaneModeOn ->
lifecycle = viewLifecycleOwner.lifecycle,
) { isAirplaneModeOn: Boolean ->
if (isAirplaneModeOn) { if (isAirplaneModeOn) {
finish() finish()
} }

View File

@@ -16,7 +16,7 @@
package com.android.settings.network.telephony; package com.android.settings.network.telephony;
import static com.android.settings.network.MobileNetworkListFragment.observeAirplaneModeAndFinishIfOn; import static com.android.settings.network.MobileNetworkListFragment.collectAirplaneModeAndFinishIfOn;
import android.app.Activity; import android.app.Activity;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
@@ -334,7 +334,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
observeAirplaneModeAndFinishIfOn(this); collectAirplaneModeAndFinishIfOn(this);
} }
@Override @Override

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 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.utils
import android.content.ContentResolver
import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun Context.observeSettingsGlobalBoolean(
name: String,
lifecycle: Lifecycle,
onChange: (newValue: Boolean) -> Unit,
) {
val field by settingsGlobalBoolean(name)
val contentObserver = object : ContentObserver(Handler.getMain()) {
override fun onChange(selfChange: Boolean) {
onChange(field)
}
}
val uri = Settings.Global.getUriFor(name)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
contentResolver.registerContentObserver(uri, false, contentObserver)
onChange(field)
}
override fun onStop(owner: LifecycleOwner) {
contentResolver.unregisterContentObserver(contentObserver)
}
})
}
fun Context.settingsGlobalBoolean(name: String): ReadWriteProperty<Any?, Boolean> =
SettingsGlobalBooleanDelegate(this, name)
private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) :
ReadWriteProperty<Any?, Boolean> {
private val contentResolver: ContentResolver = context.contentResolver
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
Settings.Global.getInt(contentResolver, name, 0) != 0
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
Settings.Global.putInt(contentResolver, name, if (value) 1 else 0)
}
}

View File

@@ -23,22 +23,18 @@ import android.os.Bundle
import android.os.UserManager import android.os.UserManager
import android.provider.Settings import android.provider.Settings
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.android.settings.datausage.DataUsageListTest.ShadowDataUsageBaseFragment import com.android.settings.datausage.DataUsageListTest.ShadowDataUsageBaseFragment
import com.android.settings.datausage.TemplatePreference.NetworkServices import com.android.settings.datausage.TemplatePreference.NetworkServices
import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.BillingCycleRepository
import com.android.settings.network.MobileDataEnabledListener
import com.android.settings.testutils.FakeFeatureFactory import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.NetworkPolicyEditor import com.android.settingslib.NetworkPolicyEditor
import com.android.settingslib.core.AbstractPreferenceController import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.doNothing import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
@@ -61,9 +57,6 @@ class DataUsageListTest {
@get:Rule @get:Rule
val mockito: MockitoRule = MockitoJUnit.rule() val mockito: MockitoRule = MockitoJUnit.rule()
@Mock
private lateinit var mobileDataEnabledListener: MobileDataEnabledListener
@Mock @Mock
private lateinit var networkServices: NetworkServices private lateinit var networkServices: NetworkServices
@@ -86,7 +79,6 @@ class DataUsageListTest {
fun setUp() { fun setUp() {
FakeFeatureFactory.setupForTest() FakeFeatureFactory.setupForTest()
networkServices.mPolicyEditor = mock(NetworkPolicyEditor::class.java) networkServices.mPolicyEditor = mock(NetworkPolicyEditor::class.java)
dataUsageList.dataStateListener = mobileDataEnabledListener
doReturn(context).`when`(dataUsageList).context doReturn(context).`when`(dataUsageList).context
doReturn(userManager).`when`(context).getSystemService(UserManager::class.java) doReturn(userManager).`when`(context).getSystemService(UserManager::class.java)
doReturn(false).`when`(userManager).isGuestUser doReturn(false).`when`(userManager).isGuestUser
@@ -112,46 +104,6 @@ class DataUsageListTest {
verify(dataUsageList).finish() verify(dataUsageList).finish()
} }
@Test
fun resume_shouldListenDataStateChange() {
dataUsageList.template = mock(NetworkTemplate::class.java)
dataUsageList.onCreate(null)
dataUsageList.dataStateListener = mobileDataEnabledListener
ReflectionHelpers.setField(
dataUsageList,
"mVisibilityLoggerMixin",
mock(VisibilityLoggerMixin::class.java),
)
ReflectionHelpers.setField(
dataUsageList,
"mPreferenceManager",
mock(PreferenceManager::class.java),
)
dataUsageList.onResume()
verify(mobileDataEnabledListener).start(ArgumentMatchers.anyInt())
dataUsageList.onPause()
}
@Test
fun pause_shouldUnlistenDataStateChange() {
dataUsageList.template = mock(NetworkTemplate::class.java)
dataUsageList.onCreate(null)
dataUsageList.dataStateListener = mobileDataEnabledListener
ReflectionHelpers.setField(
dataUsageList, "mVisibilityLoggerMixin", mock(
VisibilityLoggerMixin::class.java
)
)
ReflectionHelpers.setField(
dataUsageList, "mPreferenceManager", mock(
PreferenceManager::class.java
)
)
dataUsageList.onResume()
dataUsageList.onPause()
verify(mobileDataEnabledListener).stop()
}
@Test @Test
fun processArgument_shouldGetTemplateFromArgument() { fun processArgument_shouldGetTemplateFromArgument() {
val args = Bundle() val args = Bundle()

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2023 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.provider.Settings
import android.telephony.SubscriptionManager
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.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean
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
@RunWith(AndroidJUnit4::class)
class MobileDataEnabledFlowTest {
private val context: Context = ApplicationProvider.getApplicationContext()
@Test
fun mobileDataEnabledFlow_notified(): Unit = runBlocking {
val flow = context.mobileDataEnabledFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
assertThat(flow.firstWithTimeoutOrNull()).isNotNull()
}
@Test
fun mobileDataEnabledFlow_changed_notified(): Unit = runBlocking {
var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
mobileDataEnabled = false
val flow = context.mobileDataEnabledFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
mobileDataEnabled = true
assertThat(flow.firstWithTimeoutOrNull()).isNotNull()
}
@Test
fun mobileDataEnabledFlow_forSubIdNotChanged(): Unit = runBlocking {
var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
mobileDataEnabled = false
var mobileDataEnabledForSubId
by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA + SUB_ID)
mobileDataEnabledForSubId = false
val listDeferred = async {
context.mobileDataEnabledFlow(SUB_ID).toListWithTimeout()
}
assertThat(listDeferred.await()).hasSize(1)
}
@Test
fun mobileDataEnabledFlow_forSubIdChanged(): Unit = runBlocking {
var mobileDataEnabled by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA)
mobileDataEnabled = false
var mobileDataEnabledForSubId
by context.settingsGlobalBoolean(Settings.Global.MOBILE_DATA + SUB_ID)
mobileDataEnabledForSubId = false
val listDeferred = async {
context.mobileDataEnabledFlow(SUB_ID).toListWithTimeout()
}
delay(100)
mobileDataEnabledForSubId = true
assertThat(listDeferred.await()).hasSize(2)
}
private companion object {
const val SUB_ID = 123
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) 2023 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.utils
import android.content.Context
import android.provider.Settings
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SettingsGlobalBooleanDelegateTest {
private val context: Context = ApplicationProvider.getApplicationContext()
@Test
fun getValue_setTrue_returnTrue() {
Settings.Global.putInt(context.contentResolver, TEST_NAME, 1)
val value by context.settingsGlobalBoolean(TEST_NAME)
assertThat(value).isTrue()
}
@Test
fun getValue_setFalse_returnFalse() {
Settings.Global.putInt(context.contentResolver, TEST_NAME, 0)
val value by context.settingsGlobalBoolean(TEST_NAME)
assertThat(value).isFalse()
}
@Test
fun setValue_setTrue_returnTrue() {
var value by context.settingsGlobalBoolean(TEST_NAME)
value = true
assertThat(Settings.Global.getInt(context.contentResolver, TEST_NAME, 0)).isEqualTo(1)
}
@Test
fun setValue_setFalse_returnFalse() {
var value by context.settingsGlobalBoolean(TEST_NAME)
value = false
assertThat(Settings.Global.getInt(context.contentResolver, TEST_NAME, 1)).isEqualTo(0)
}
@Test
fun observeSettingsGlobalBoolean_valueNotChanged() {
var value by context.settingsGlobalBoolean(TEST_NAME)
value = false
var newValue: Boolean? = null
context.observeSettingsGlobalBoolean(TEST_NAME, TestLifecycleOwner().lifecycle) {
newValue = it
}
assertThat(newValue).isFalse()
}
@Test
fun observeSettingsGlobalBoolean_valueChanged() {
var value by context.settingsGlobalBoolean(TEST_NAME)
value = false
var newValue: Boolean? = null
context.observeSettingsGlobalBoolean(TEST_NAME, TestLifecycleOwner().lifecycle) {
newValue = it
}
value = true
assertThat(newValue).isFalse()
}
private companion object {
const val TEST_NAME = "test_boolean_delegate"
}
}