diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt index 7240150d901..30e8db3776e 100644 --- a/src/com/android/settings/datausage/DataUsageList.kt +++ b/src/com/android/settings/datausage/DataUsageList.kt @@ -31,9 +31,10 @@ import androidx.preference.Preference import com.android.settings.R import com.android.settings.datausage.lib.BillingCycleRepository import com.android.settings.datausage.lib.NetworkUsageData -import com.android.settings.network.MobileDataEnabledListener import com.android.settings.network.MobileNetworkRepository +import com.android.settings.network.mobileDataEnabledFlow 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.utils.ThreadUtils import kotlin.jvm.optionals.getOrNull @@ -43,10 +44,7 @@ import kotlin.jvm.optionals.getOrNull * to inspect based on usage cycle and control through [NetworkPolicy]. */ @OpenForTesting -open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Client { - @VisibleForTesting - lateinit var dataStateListener: MobileDataEnabledListener - +open class DataUsageList : DataUsageBaseFragment() { @JvmField @VisibleForTesting var template: NetworkTemplate? = null @@ -89,7 +87,6 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl return } updateSubscriptionInfoEntity() - dataStateListener = MobileDataEnabledListener(activity, this) dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply { init(template) } @@ -103,6 +100,9 @@ open class DataUsageList : DataUsageBaseFragment(), MobileDataEnabledListener.Cl override fun onViewCreated(v: View, savedInstanceState: Bundle?) { super.onViewCreated(v, savedInstanceState) + requireContext().mobileDataEnabledFlow(subId) + .collectLatestWithLifecycle(viewLifecycleOwner) { updatePolicy() } + val template = template ?: return dataUsageListHeaderController = DataUsageListHeaderController( 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 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]. */ @VisibleForTesting fun updatePolicy() { diff --git a/src/com/android/settings/network/MobileDataEnabledFlow.kt b/src/com/android/settings/network/MobileDataEnabledFlow.kt new file mode 100644 index 00000000000..23423771f1f --- /dev/null +++ b/src/com/android/settings/network/MobileDataEnabledFlow.kt @@ -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 { + 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) + } + } +} diff --git a/src/com/android/settings/network/MobileDataEnabledListener.java b/src/com/android/settings/network/MobileDataEnabledListener.java index b0308238cb3..f2d55ab6dec 100644 --- a/src/com/android/settings/network/MobileDataEnabledListener.java +++ b/src/com/android/settings/network/MobileDataEnabledListener.java @@ -20,7 +20,12 @@ import android.content.Context; import android.provider.Settings; 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 { private Context mContext; private Client mClient; diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt index 5000afdf0a3..09b1150d196 100644 --- a/src/com/android/settings/network/MobileNetworkListFragment.kt +++ b/src/com/android/settings/network/MobileNetworkListFragment.kt @@ -28,15 +28,16 @@ import com.android.settings.SettingsPreferenceFragment import com.android.settings.dashboard.DashboardFragment import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.search.BaseSearchIndexProvider -import com.android.settings.utils.observeSettingsGlobalBoolean 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.settingsprovider.settingsGlobalBooleanFlow @SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv()) class MobileNetworkListFragment : DashboardFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - observeAirplaneModeAndFinishIfOn() + collectAirplaneModeAndFinishIfOn() } override fun onResume() { @@ -59,15 +60,13 @@ class MobileNetworkListFragment : DashboardFragment() { private const val KEY_ADD_SIM = "add_sim" @JvmStatic - fun SettingsPreferenceFragment.observeAirplaneModeAndFinishIfOn() { - requireContext().observeSettingsGlobalBoolean( - name = Settings.Global.AIRPLANE_MODE_ON, - lifecycle = viewLifecycleOwner.lifecycle, - ) { isAirplaneModeOn: Boolean -> - if (isAirplaneModeOn) { - finish() + fun SettingsPreferenceFragment.collectAirplaneModeAndFinishIfOn() { + requireContext().settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON) + .collectLatestWithLifecycle(viewLifecycleOwner) { isAirplaneModeOn -> + if (isAirplaneModeOn) { + finish() + } } - } } @JvmField diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 7e290b829ed..a51441423c1 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -16,7 +16,7 @@ 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.settings.SettingsEnums; @@ -334,7 +334,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - observeAirplaneModeAndFinishIfOn(this); + collectAirplaneModeAndFinishIfOn(this); } @Override diff --git a/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt b/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt deleted file mode 100644 index fdfbdb4ecfa..00000000000 --- a/src/com/android/settings/utils/SettingsGlobalBooleanDelegate.kt +++ /dev/null @@ -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 = - SettingsGlobalBooleanDelegate(this, name) - -private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) : - ReadWriteProperty { - - 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) - } -} diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt index 90bb0487b3a..39b844680d2 100644 --- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt +++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt @@ -23,22 +23,18 @@ import android.os.Bundle import android.os.UserManager import android.provider.Settings import androidx.preference.Preference -import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider import com.android.settings.datausage.DataUsageListTest.ShadowDataUsageBaseFragment import com.android.settings.datausage.TemplatePreference.NetworkServices import com.android.settings.datausage.lib.BillingCycleRepository -import com.android.settings.network.MobileDataEnabledListener import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.NetworkPolicyEditor import com.android.settingslib.core.AbstractPreferenceController -import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.doNothing import org.mockito.Mockito.doReturn @@ -61,9 +57,6 @@ class DataUsageListTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @Mock - private lateinit var mobileDataEnabledListener: MobileDataEnabledListener - @Mock private lateinit var networkServices: NetworkServices @@ -86,7 +79,6 @@ class DataUsageListTest { fun setUp() { FakeFeatureFactory.setupForTest() networkServices.mPolicyEditor = mock(NetworkPolicyEditor::class.java) - dataUsageList.dataStateListener = mobileDataEnabledListener doReturn(context).`when`(dataUsageList).context doReturn(userManager).`when`(context).getSystemService(UserManager::class.java) doReturn(false).`when`(userManager).isGuestUser @@ -112,46 +104,6 @@ class DataUsageListTest { 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 fun processArgument_shouldGetTemplateFromArgument() { val args = Bundle() diff --git a/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt new file mode 100644 index 00000000000..48623097e69 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/MobileDataEnabledFlowTest.kt @@ -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 + } +} diff --git a/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt b/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt deleted file mode 100644 index 75c368561af..00000000000 --- a/tests/spa_unit/src/com/android/settings/utils/SettingsGlobalBooleanDelegateTest.kt +++ /dev/null @@ -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" - } -}