Merge "New MobileDataEnabledFlow" into main
diff --git a/src/com/android/settings/datausage/DataUsageList.kt b/src/com/android/settings/datausage/DataUsageList.kt
index 7240150..30e8db3 100644
--- a/src/com/android/settings/datausage/DataUsageList.kt
+++ b/src/com/android/settings/datausage/DataUsageList.kt
@@ -31,9 +31,10 @@
 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 @@
  * 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 @@
             return
         }
         updateSubscriptionInfoEntity()
-        dataStateListener = MobileDataEnabledListener(activity, this)
         dataUsageListAppsController = use(DataUsageListAppsController::class.java).apply {
             init(template)
         }
@@ -103,6 +100,9 @@
     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 @@
         )
     }
 
-    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 @@
         }
     }
 
-    /**
-     * 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 0000000..2342377
--- /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<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)
+        }
+    }
+}
diff --git a/src/com/android/settings/network/MobileDataEnabledListener.java b/src/com/android/settings/network/MobileDataEnabledListener.java
index b030823..f2d55ab 100644
--- a/src/com/android/settings/network/MobileDataEnabledListener.java
+++ b/src/com/android/settings/network/MobileDataEnabledListener.java
@@ -20,7 +20,12 @@
 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 5000afd..09b1150 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.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 @@
         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 7e290b8..a514414 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 @@
     @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 fdfbdb4..0000000
--- 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<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)
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.kt
index 90bb048..39b8446 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.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
@@ -62,9 +58,6 @@
     val mockito: MockitoRule = MockitoJUnit.rule()
 
     @Mock
-    private lateinit var mobileDataEnabledListener: MobileDataEnabledListener
-
-    @Mock
     private lateinit var networkServices: NetworkServices
 
     @Mock
@@ -86,7 +79,6 @@
     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
@@ -113,46 +105,6 @@
     }
 
     @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()
         args.putParcelable(
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 0000000..4862309
--- /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 75c3685..0000000
--- 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"
-    }
-}