diff --git a/src/com/android/settings/network/MobileDataEnabledListener.java b/src/com/android/settings/network/MobileDataEnabledListener.java new file mode 100644 index 00000000000..8344f886f7f --- /dev/null +++ b/src/com/android/settings/network/MobileDataEnabledListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 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.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.telephony.SubscriptionManager; + +/** Helper class to listen for changes in the enabled state of mobile data. */ +public class MobileDataEnabledListener extends ContentObserver { + private Context mContext; + private Client mClient; + private int mSubId; + + public interface Client { + void onMobileDataEnabledChange(); + } + + public MobileDataEnabledListener(Context context, Client client) { + super(new Handler()); + mContext = context; + mClient = client; + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + /** Starts listening to changes in the enabled state for data on the given subscription id. */ + public void start(int subId) { + mSubId = subId; + Uri uri; + if (mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + } else { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + mSubId); + } + mContext.getContentResolver().registerContentObserver(uri, true /*notifyForDescendants*/, + this); + } + + public int getSubId() { + return mSubId; + } + + public MobileDataEnabledListener stop() { + mContext.getContentResolver().unregisterContentObserver(this); + return this; + } + + @Override + public void onChange(boolean selfChange) { + mClient.onMobileDataEnabledChange(); + } +} diff --git a/src/com/android/settings/network/SubscriptionsPreferenceController.java b/src/com/android/settings/network/SubscriptionsPreferenceController.java index 88ff5cf247f..ae31b5643a9 100644 --- a/src/com/android/settings/network/SubscriptionsPreferenceController.java +++ b/src/com/android/settings/network/SubscriptionsPreferenceController.java @@ -21,6 +21,9 @@ import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; import android.content.Context; import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; import android.provider.Settings; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -35,6 +38,7 @@ import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.network.telephony.DataConnectivityListener; import com.android.settings.network.telephony.MobileNetworkActivity; import com.android.settingslib.core.AbstractPreferenceController; @@ -46,14 +50,18 @@ import java.util.Map; * available if there are 2 or more subscriptions. */ public class SubscriptionsPreferenceController extends AbstractPreferenceController implements - LifecycleObserver, SubscriptionsChangeListener.SubscriptionsChangeListenerClient { + LifecycleObserver, SubscriptionsChangeListener.SubscriptionsChangeListenerClient, + MobileDataEnabledListener.Client, DataConnectivityListener.Client { private static final String TAG = "SubscriptionsPrefCntrlr"; private UpdateListener mUpdateListener; private String mPreferenceGroupKey; private PreferenceGroup mPreferenceGroup; private SubscriptionManager mManager; + private ConnectivityManager mConnectivityManager; private SubscriptionsChangeListener mSubscriptionsListener; + private MobileDataEnabledListener mDataEnabledListener; + private DataConnectivityListener mConnectivityListener; // Map of subscription id to Preference private Map mSubscriptionPreferences; @@ -89,20 +97,27 @@ public class SubscriptionsPreferenceController extends AbstractPreferenceControl mPreferenceGroupKey = preferenceGroupKey; mStartOrder = startOrder; mManager = context.getSystemService(SubscriptionManager.class); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); mSubscriptionPreferences = new ArrayMap<>(); mSubscriptionsListener = new SubscriptionsChangeListener(context, this); + mDataEnabledListener = new MobileDataEnabledListener(context, this); + mConnectivityListener = new DataConnectivityListener(context, this); lifecycle.addObserver(this); } @OnLifecycleEvent(ON_RESUME) public void onResume() { mSubscriptionsListener.start(); + mDataEnabledListener.start(SubscriptionManager.getDefaultDataSubscriptionId()); + mConnectivityListener.start(); update(); } @OnLifecycleEvent(ON_PAUSE) public void onPause() { mSubscriptionsListener.stop(); + mDataEnabledListener.stop(); + mConnectivityListener.stop(); } @Override @@ -158,6 +173,19 @@ public class SubscriptionsPreferenceController extends AbstractPreferenceControl mUpdateListener.onChildrenUpdated(); } + private boolean activeNetworkIsCellular() { + final Network activeNetwork = mConnectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + return false; + } + final NetworkCapabilities networkCapabilities = mConnectivityManager.getNetworkCapabilities( + activeNetwork); + if (networkCapabilities == null) { + return false; + } + return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + } + /** * The summary can have either 1 or 2 lines depending on which services (calls, SMS, data) this * subscription is the default for. @@ -187,10 +215,10 @@ public class SubscriptionsPreferenceController extends AbstractPreferenceControl if (subId == dataDefaultSubId) { final TelephonyManager telMgrForSub = mContext.getSystemService( TelephonyManager.class).createForSubscriptionId(subId); - final int dataState = telMgrForSub.getDataState(); - if (dataState == TelephonyManager.DATA_CONNECTED) { + boolean dataEnabled = telMgrForSub.isDataEnabled(); + if (dataEnabled && activeNetworkIsCellular()) { line2 = mContext.getString(R.string.mobile_data_active); - } else if (!telMgrForSub.isDataEnabled()) { + } else if (!dataEnabled) { line2 = mContext.getString(R.string.mobile_data_off); } else { line2 = mContext.getString(R.string.default_for_mobile_data); @@ -231,6 +259,22 @@ public class SubscriptionsPreferenceController extends AbstractPreferenceControl @Override public void onSubscriptionsChanged() { + // See if we need to change which sub id we're using to listen for enabled/disabled changes. + int defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); + if (defaultDataSubId != mDataEnabledListener.getSubId()) { + mDataEnabledListener.stop(); + mDataEnabledListener.start(defaultDataSubId); + } + update(); + } + + @Override + public void onMobileDataEnabledChange() { + update(); + } + + @Override + public void onDataConnectivityChange() { update(); } } diff --git a/src/com/android/settings/network/telephony/DataConnectivityListener.java b/src/com/android/settings/network/telephony/DataConnectivityListener.java new file mode 100644 index 00000000000..adb39c64be0 --- /dev/null +++ b/src/com/android/settings/network/telephony/DataConnectivityListener.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; + +/** A helper class to listen to a few different kinds of connectivity changes that could be relevant + * to changes in which network is active, and whether the active network has internet data + * connectivity. */ +public class DataConnectivityListener extends ConnectivityManager.NetworkCallback { + private Context mContext; + private ConnectivityManager mConnectivityManager; + private final NetworkRequest mNetworkRequest; + private Client mClient; + + public interface Client { + void onDataConnectivityChange(); + } + + public DataConnectivityListener(Context context, Client client) { + mContext = context; + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mClient = client; + mNetworkRequest = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + } + + public void start() { + mConnectivityManager.registerNetworkCallback(mNetworkRequest, this, + mContext.getMainThreadHandler()); + } + + public void stop() { + mConnectivityManager.unregisterNetworkCallback(this); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + final Network activeNetwork = mConnectivityManager.getActiveNetwork(); + if (activeNetwork != null && activeNetwork.equals(network)) { + mClient.onDataConnectivityChange(); + } + } + + @Override + public void onLosing(Network network, int maxMsToLive) { + mClient.onDataConnectivityChange(); + } + + @Override + public void onLost(Network network) { + mClient.onDataConnectivityChange(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/DataConnectivityListenerTest.java b/tests/robotests/src/com/android/settings/network/DataConnectivityListenerTest.java new file mode 100644 index 00000000000..b45e28c554c --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/DataConnectivityListenerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2019 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; + +import com.android.settings.network.telephony.DataConnectivityListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class DataConnectivityListenerTest { + @Mock + private DataConnectivityListener.Client mClient; + @Mock + private ConnectivityManager mConnectivityManager; + @Mock + private Network mActiveNetwork; + + private Context mContext; + private DataConnectivityListener mListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityManager); + when(mConnectivityManager.getActiveNetwork()).thenReturn(mActiveNetwork); + mListener = new DataConnectivityListener(mContext, mClient); + } + + @Test + public void noStart_doesNotRegister() { + verify(mConnectivityManager, never()).registerNetworkCallback(any(NetworkRequest.class), + any(ConnectivityManager.NetworkCallback.class), any(Handler.class)); + } + + @Test + public void start_doesRegister() { + mListener.start(); + verify(mConnectivityManager).registerNetworkCallback(any(NetworkRequest.class), + eq(mListener), any(Handler.class)); + } + + @Test + public void onCapabilitiesChanged_notActiveNetwork_noCallback() { + Network changedNetwork = mock(Network.class); + mListener.onCapabilitiesChanged(changedNetwork, mock(NetworkCapabilities.class)); + verify(mClient, never()).onDataConnectivityChange(); + } + + @Test + public void onCapabilitiesChanged_activeNetwork_onDataConnectivityChangeFires() { + mListener.onCapabilitiesChanged(mActiveNetwork, mock(NetworkCapabilities.class)); + verify(mClient).onDataConnectivityChange(); + } + + @Test + public void onLosing_notActiveNetwork_onDataConnectivityChangeFires() { + Network changedNetwork = mock(Network.class); + mListener.onLosing(changedNetwork, 500); + verify(mClient).onDataConnectivityChange(); + } + + @Test + public void onLosing_activeNetwork_onDataConnectivityChangeFires() { + mListener.onLosing(mActiveNetwork, 500); + verify(mClient).onDataConnectivityChange(); + } + + @Test + public void onLost_notActiveNetwork_onDataConnectivityChangeFires() { + Network changedNetwork = mock(Network.class); + mListener.onLost(changedNetwork); + verify(mClient).onDataConnectivityChange(); + } + + @Test + public void onLost_activeNetwork_onDataConnectivityChangeFires() { + mListener.onLost(mActiveNetwork); + verify(mClient).onDataConnectivityChange(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/MobileDataEnabledListenerTest.java b/tests/robotests/src/com/android/settings/network/MobileDataEnabledListenerTest.java new file mode 100644 index 00000000000..0824680bf88 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/MobileDataEnabledListenerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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 static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class MobileDataEnabledListenerTest { + private static final int SUB_ID_ONE = 111; + private static final int SUB_ID_TWO = 222; + + @Mock + private MobileDataEnabledListener.Client mClient; + + private Context mContext; + private MobileDataEnabledListener mListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mListener = new MobileDataEnabledListener(mContext, mClient); + } + + @Test + public void onMobileDataEnabledChange_firesCorrectly() { + mListener.start(SUB_ID_ONE); + final Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + SUB_ID_ONE); + mContext.getContentResolver().notifyChange(uri, null); + verify(mClient).onMobileDataEnabledChange(); + } + + @Test + public void onMobileDataEnabledChange_doesNotFireAfterStop() { + mListener.start(SUB_ID_ONE); + mListener.stop(); + final Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + SUB_ID_ONE); + mContext.getContentResolver().notifyChange(uri, null); + verify(mClient, never()).onMobileDataEnabledChange(); + } + + @Test + public void onMobileDataEnabledChange_changedToDifferentId_firesCorrectly() { + mListener.start(SUB_ID_ONE); + mListener.stop(); + mListener.start(SUB_ID_TWO); + final Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + SUB_ID_TWO); + mContext.getContentResolver().notifyChange(uri, null); + verify(mClient).onMobileDataEnabledChange(); + } +} diff --git a/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java index 26f26ff5505..d6edcc72a87 100644 --- a/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java @@ -33,6 +33,9 @@ import static org.mockito.Mockito.when; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; import android.provider.Settings; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -74,7 +77,13 @@ public class SubscriptionsPreferenceControllerTest { @Mock private SubscriptionManager mSubscriptionManager; @Mock + private ConnectivityManager mConnectivityManager; + @Mock private TelephonyManager mTelephonyManager; + @Mock + private Network mActiveNetwork; + @Mock + private NetworkCapabilities mCapabilities; private Context mContext; private LifecycleOwner mLifecycleOwner; @@ -90,7 +99,10 @@ public class SubscriptionsPreferenceControllerTest { mLifecycleOwner = () -> mLifecycle; mLifecycle = new Lifecycle(mLifecycleOwner); when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); + when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityManager); when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mConnectivityManager.getActiveNetwork()).thenReturn(mActiveNetwork); + when(mConnectivityManager.getNetworkCapabilities(mActiveNetwork)).thenReturn(mCapabilities); when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); when(mScreen.findPreference(eq(KEY))).thenReturn(mPreferenceCategory); when(mPreferenceCategory.getContext()).thenReturn(mContext); @@ -308,7 +320,8 @@ public class SubscriptionsPreferenceControllerTest { ShadowSubscriptionManager.setDefaultDataSubscriptionId(11); ShadowSubscriptionManager.setDefaultSmsSubscriptionId(11); ShadowSubscriptionManager.setDefaultVoiceSubscriptionId(11); - when(mTelephonyManager.getDataState()).thenReturn(TelephonyManager.DATA_CONNECTED); + when(mTelephonyManager.isDataEnabled()).thenReturn(true); + when(mCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)).thenReturn(true); assertThat(mController.getSummary(11)).isEqualTo( mContext.getString(R.string.default_for_calls_and_sms) + System.lineSeparator() @@ -318,6 +331,27 @@ public class SubscriptionsPreferenceControllerTest { mContext.getString(R.string.subscription_available)); } + @Test + public void getSummary_twoSubsOneDefaultForEverythingDataNotActive() { + final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); + final SubscriptionInfo sub2 = mock(SubscriptionInfo.class); + when(sub1.getSubscriptionId()).thenReturn(11); + when(sub2.getSubscriptionId()).thenReturn(22); + SubscriptionUtil.setActiveSubscriptionsForTesting(Arrays.asList(sub1, sub2)); + + ShadowSubscriptionManager.setDefaultDataSubscriptionId(11); + ShadowSubscriptionManager.setDefaultSmsSubscriptionId(11); + ShadowSubscriptionManager.setDefaultVoiceSubscriptionId(11); + when(mTelephonyManager.isDataEnabled()).thenReturn(true); + + assertThat(mController.getSummary(11)).isEqualTo( + mContext.getString(R.string.default_for_calls_and_sms) + System.lineSeparator() + + mContext.getString(R.string.default_for_mobile_data)); + + assertThat(mController.getSummary(22)).isEqualTo( + mContext.getString(R.string.subscription_available)); + } + @Test public void getSummary_twoSubsOneDefaultForEverythingDataDisabled() { final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); @@ -329,7 +363,6 @@ public class SubscriptionsPreferenceControllerTest { ShadowSubscriptionManager.setDefaultVoiceSubscriptionId(11); ShadowSubscriptionManager.setDefaultSmsSubscriptionId(11); ShadowSubscriptionManager.setDefaultDataSubscriptionId(11); - when(mTelephonyManager.getDataState()).thenReturn(TelephonyManager.DATA_DISCONNECTED); when(mTelephonyManager.isDataEnabled()).thenReturn(false); assertThat(mController.getSummary(11)).isEqualTo( @@ -351,7 +384,6 @@ public class SubscriptionsPreferenceControllerTest { ShadowSubscriptionManager.setDefaultDataSubscriptionId(11); ShadowSubscriptionManager.setDefaultSmsSubscriptionId(22); ShadowSubscriptionManager.setDefaultVoiceSubscriptionId(11); - when(mTelephonyManager.getDataState()).thenReturn(TelephonyManager.DATA_DISCONNECTED); when(mTelephonyManager.isDataEnabled()).thenReturn(true); assertThat(mController.getSummary(11)).isEqualTo(