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.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() {

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.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;

View File

@@ -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

View File

@@ -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

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.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()

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"
}
}