diff options
6 files changed, 326 insertions, 20 deletions
diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java index 7bc6056f91f3..70a8a7ff7bb3 100644 --- a/services/core/java/com/android/server/vcn/Vcn.java +++ b/services/core/java/com/android/server/vcn/Vcn.java @@ -16,6 +16,8 @@ package com.android.server.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; @@ -26,14 +28,20 @@ import static com.android.server.VcnManagementService.VDBG; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ContentResolver; +import android.database.ContentObserver; import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.net.Uri; import android.net.vcn.VcnConfig; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnManager.VcnErrorCode; import android.os.Handler; import android.os.Message; import android.os.ParcelUuid; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -42,9 +50,11 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.server.VcnManagementService.VcnCallback; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -61,6 +71,9 @@ import java.util.Set; public class Vcn extends Handler { private static final String TAG = Vcn.class.getSimpleName(); + private static final List<Integer> CAPS_REQUIRING_MOBILE_DATA = + Arrays.asList(NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN); + private static final int MSG_EVENT_BASE = 0; private static final int MSG_CMD_BASE = 100; @@ -110,6 +123,15 @@ public class Vcn extends Handler { */ private static final int MSG_EVENT_SAFE_MODE_STATE_CHANGED = MSG_EVENT_BASE + 4; + /** + * Triggers reevaluation of mobile data enabled conditions. + * + * <p>Upon this notification, the VCN will check if any of the underlying subIds have mobile + * data enabled. If not, the VCN will restart any GatewayConnections providing INTERNET or DUN + * with the current mobile data toggle status. + */ + private static final int MSG_EVENT_MOBILE_DATA_TOGGLED = MSG_EVENT_BASE + 5; + /** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */ private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE; @@ -118,6 +140,8 @@ public class Vcn extends Handler { @NonNull private final Dependencies mDeps; @NonNull private final VcnNetworkRequestListener mRequestListener; @NonNull private final VcnCallback mVcnCallback; + @NonNull private final VcnContentResolver mContentResolver; + @NonNull private final ContentObserver mMobileDataSettingsObserver; /** * Map containing all VcnGatewayConnections and their VcnGatewayConnectionConfigs. @@ -154,6 +178,8 @@ public class Vcn extends Handler { // Accessed from different threads, but always under lock in VcnManagementService private volatile int mCurrentStatus = VCN_STATUS_CODE_ACTIVE; + private boolean mIsMobileDataEnabled = false; + public Vcn( @NonNull VcnContext vcnContext, @NonNull ParcelUuid subscriptionGroup, @@ -177,10 +203,19 @@ public class Vcn extends Handler { mVcnCallback = Objects.requireNonNull(vcnCallback, "Missing vcnCallback"); mDeps = Objects.requireNonNull(deps, "Missing deps"); mRequestListener = new VcnNetworkRequestListener(); + mContentResolver = mDeps.newVcnContentResolver(mVcnContext); + mMobileDataSettingsObserver = new VcnMobileDataContentObserver(this /* handler */); + + final Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + mContentResolver.registerContentObserver( + uri, true /* notifyForDescendants */, mMobileDataSettingsObserver); mConfig = Objects.requireNonNull(config, "Missing config"); mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); + // Update mIsMobileDataEnabled before starting handling of NetworkRequests. + mIsMobileDataEnabled = getMobileDataStatus(); + // Register to receive cached and future NetworkRequests mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener); } @@ -260,6 +295,9 @@ public class Vcn extends Handler { case MSG_EVENT_SAFE_MODE_STATE_CHANGED: handleSafeModeStatusChanged(); break; + case MSG_EVENT_MOBILE_DATA_TOGGLED: + handleMobileDataToggled(); + break; case MSG_CMD_TEARDOWN: handleTeardown(); break; @@ -366,18 +404,37 @@ public class Vcn extends Handler { if (isRequestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) { Slog.v(getLogTag(), "Bringing up new VcnGatewayConnection for request " + request); + if (getExposedCapabilitiesForMobileDataState(gatewayConnectionConfig).isEmpty()) { + // Skip; this network does not provide any services if mobile data is disabled. + continue; + } + final VcnGatewayConnection vcnGatewayConnection = mDeps.newVcnGatewayConnection( mVcnContext, mSubscriptionGroup, mLastSnapshot, gatewayConnectionConfig, - new VcnGatewayStatusCallbackImpl(gatewayConnectionConfig)); + new VcnGatewayStatusCallbackImpl(gatewayConnectionConfig), + mIsMobileDataEnabled); mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection); } } } + private Set<Integer> getExposedCapabilitiesForMobileDataState( + VcnGatewayConnectionConfig gatewayConnectionConfig) { + if (mIsMobileDataEnabled) { + return gatewayConnectionConfig.getAllExposedCapabilities(); + } + + final Set<Integer> exposedCapsWithoutMobileData = + new ArraySet<>(gatewayConnectionConfig.getAllExposedCapabilities()); + exposedCapsWithoutMobileData.removeAll(CAPS_REQUIRING_MOBILE_DATA); + + return exposedCapsWithoutMobileData; + } + private void handleGatewayConnectionQuit(VcnGatewayConnectionConfig config) { Slog.v(getLogTag(), "VcnGatewayConnection quit: " + config); mVcnGatewayConnections.remove(config); @@ -396,12 +453,55 @@ public class Vcn extends Handler { } } + private void handleMobileDataToggled() { + final boolean oldMobileDataEnabledStatus = mIsMobileDataEnabled; + mIsMobileDataEnabled = getMobileDataStatus(); + + if (oldMobileDataEnabledStatus != mIsMobileDataEnabled) { + // Teardown any GatewayConnections that advertise INTERNET or DUN. If they provide other + // services, the VcnGatewayConnections will be restarted without advertising INTERNET or + // DUN. + for (Entry<VcnGatewayConnectionConfig, VcnGatewayConnection> entry : + mVcnGatewayConnections.entrySet()) { + final VcnGatewayConnectionConfig gatewayConnectionConfig = entry.getKey(); + final VcnGatewayConnection gatewayConnection = entry.getValue(); + + final Set<Integer> exposedCaps = + gatewayConnectionConfig.getAllExposedCapabilities(); + if (exposedCaps.contains(NET_CAPABILITY_INTERNET) + || exposedCaps.contains(NET_CAPABILITY_DUN)) { + if (gatewayConnection == null) { + Slog.wtf( + getLogTag(), + "Found gatewayConnectionConfig without GatewayConnection"); + } else { + // TODO(b/184868850): Optimize by restarting NetworkAgents without teardown. + gatewayConnection.teardownAsynchronously(); + } + } + } + } + } + + private boolean getMobileDataStatus() { + final TelephonyManager genericTelMan = + mVcnContext.getContext().getSystemService(TelephonyManager.class); + + for (int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) { + if (genericTelMan.createForSubscriptionId(subId).isDataEnabled()) { + return true; + } + } + + return false; + } + private boolean isRequestSatisfiedByGatewayConnectionConfig( @NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) { final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); builder.addTransportType(TRANSPORT_CELLULAR); builder.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); - for (int cap : config.getAllExposedCapabilities()) { + for (int cap : getExposedCapabilitiesForMobileDataState(config)) { builder.addCapability(cap); } @@ -432,6 +532,16 @@ public class Vcn extends Handler { pw.decreaseIndent(); } + @VisibleForTesting(visibility = Visibility.PRIVATE) + public boolean isMobileDataEnabled() { + return mIsMobileDataEnabled; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public void setMobileDataEnabled(boolean isMobileDataEnabled) { + mIsMobileDataEnabled = isMobileDataEnabled; + } + /** Retrieves the network score for a VCN Network */ // Package visibility for use in VcnGatewayConnection static int getNetworkScore() { @@ -485,6 +595,17 @@ public class Vcn extends Handler { } } + private class VcnMobileDataContentObserver extends ContentObserver { + private VcnMobileDataContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + sendMessage(obtainMessage(MSG_EVENT_MOBILE_DATA_TOGGLED)); + } + } + /** External dependencies used by Vcn, for injection in tests */ @VisibleForTesting(visibility = Visibility.PRIVATE) public static class Dependencies { @@ -494,13 +615,36 @@ public class Vcn extends Handler { ParcelUuid subscriptionGroup, TelephonySubscriptionSnapshot snapshot, VcnGatewayConnectionConfig connectionConfig, - VcnGatewayStatusCallback gatewayStatusCallback) { + VcnGatewayStatusCallback gatewayStatusCallback, + boolean isMobileDataEnabled) { return new VcnGatewayConnection( vcnContext, subscriptionGroup, snapshot, connectionConfig, - gatewayStatusCallback); + gatewayStatusCallback, + isMobileDataEnabled); + } + + /** Builds a new VcnContentResolver instance */ + public VcnContentResolver newVcnContentResolver(VcnContext vcnContext) { + return new VcnContentResolver(vcnContext); + } + } + + /** Proxy Implementation of NetworkAgent, used for testing. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class VcnContentResolver { + private final ContentResolver mImpl; + + public VcnContentResolver(VcnContext vcnContext) { + mImpl = vcnContext.getContext().getContentResolver(); + } + + /** Registers the content observer */ + public void registerContentObserver( + @NonNull Uri uri, boolean notifyForDescendants, @NonNull ContentObserver observer) { + mImpl.registerContentObserver(uri, notifyForDescendants, observer); } } } diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index df31221b5d60..8847dda71c49 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -16,6 +16,8 @@ package com.android.server.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; @@ -517,6 +519,7 @@ public class VcnGatewayConnection extends StateMachine { @NonNull private final VcnGatewayStatusCallback mGatewayStatusCallback; @NonNull private final Dependencies mDeps; @NonNull private final VcnUnderlyingNetworkTrackerCallback mUnderlyingNetworkTrackerCallback; + private final boolean mIsMobileDataEnabled; @NonNull private final IpSecManager mIpSecManager; @@ -626,13 +629,15 @@ public class VcnGatewayConnection extends StateMachine { @NonNull ParcelUuid subscriptionGroup, @NonNull TelephonySubscriptionSnapshot snapshot, @NonNull VcnGatewayConnectionConfig connectionConfig, - @NonNull VcnGatewayStatusCallback gatewayStatusCallback) { + @NonNull VcnGatewayStatusCallback gatewayStatusCallback, + boolean isMobileDataEnabled) { this( vcnContext, subscriptionGroup, snapshot, connectionConfig, gatewayStatusCallback, + isMobileDataEnabled, new Dependencies()); } @@ -643,6 +648,7 @@ public class VcnGatewayConnection extends StateMachine { @NonNull TelephonySubscriptionSnapshot snapshot, @NonNull VcnGatewayConnectionConfig connectionConfig, @NonNull VcnGatewayStatusCallback gatewayStatusCallback, + boolean isMobileDataEnabled, @NonNull Dependencies deps) { super(TAG, Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); mVcnContext = vcnContext; @@ -650,6 +656,7 @@ public class VcnGatewayConnection extends StateMachine { mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig"); mGatewayStatusCallback = Objects.requireNonNull(gatewayStatusCallback, "Missing gatewayStatusCallback"); + mIsMobileDataEnabled = isMobileDataEnabled; mDeps = Objects.requireNonNull(deps, "Missing deps"); mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); @@ -1502,7 +1509,7 @@ public class VcnGatewayConnection extends StateMachine { @NonNull VcnNetworkAgent agent, @NonNull VcnChildSessionConfiguration childConfig) { final NetworkCapabilities caps = - buildNetworkCapabilities(mConnectionConfig, mUnderlying); + buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); final LinkProperties lp = buildConnectedLinkProperties( mConnectionConfig, tunnelIface, childConfig, mUnderlying); @@ -1515,7 +1522,7 @@ public class VcnGatewayConnection extends StateMachine { @NonNull IpSecTunnelInterface tunnelIface, @NonNull VcnChildSessionConfiguration childConfig) { final NetworkCapabilities caps = - buildNetworkCapabilities(mConnectionConfig, mUnderlying); + buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); final LinkProperties lp = buildConnectedLinkProperties( mConnectionConfig, tunnelIface, childConfig, mUnderlying); @@ -1843,7 +1850,8 @@ public class VcnGatewayConnection extends StateMachine { @VisibleForTesting(visibility = Visibility.PRIVATE) static NetworkCapabilities buildNetworkCapabilities( @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig, - @Nullable UnderlyingNetworkRecord underlying) { + @Nullable UnderlyingNetworkRecord underlying, + boolean isMobileDataEnabled) { final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); builder.addTransportType(TRANSPORT_CELLULAR); @@ -1853,6 +1861,12 @@ public class VcnGatewayConnection extends StateMachine { // Add exposed capabilities for (int cap : gatewayConnectionConfig.getAllExposedCapabilities()) { + // Skip adding INTERNET or DUN if mobile data is disabled. + if (!isMobileDataEnabled + && (cap == NET_CAPABILITY_INTERNET || cap == NET_CAPABILITY_DUN)) { + continue; + } + builder.addCapability(cap); } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java index 5f27fabb62b0..ac0edaa3b579 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java @@ -65,6 +65,7 @@ public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnect TEST_SUBSCRIPTION_SNAPSHOT, mConfig, mGatewayStatusCallback, + true /* isMobileDataEnabled */, mDeps); vgc.setIsQuitting(true); diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java index ced8745e89c9..9705f0fc6bbc 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java @@ -16,6 +16,8 @@ package com.android.server.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; @@ -89,7 +91,8 @@ public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { doReturn(mWifiInfo).when(mWifiInfo).makeCopy(anyLong()); } - private void verifyBuildNetworkCapabilitiesCommon(int transportType) { + private void verifyBuildNetworkCapabilitiesCommon( + int transportType, boolean isMobileDataEnabled) { final NetworkCapabilities.Builder capBuilder = new NetworkCapabilities.Builder(); capBuilder.addTransportType(transportType); capBuilder.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); @@ -109,10 +112,22 @@ public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { capBuilder.build(), new LinkProperties(), false); final NetworkCapabilities vcnCaps = VcnGatewayConnection.buildNetworkCapabilities( - VcnGatewayConnectionConfigTest.buildTestConfig(), record); + VcnGatewayConnectionConfigTest.buildTestConfig(), + record, + isMobileDataEnabled); + assertTrue(vcnCaps.hasTransport(TRANSPORT_CELLULAR)); assertTrue(vcnCaps.hasCapability(NET_CAPABILITY_NOT_METERED)); assertTrue(vcnCaps.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + + for (int cap : VcnGatewayConnectionConfigTest.EXPOSED_CAPS) { + if (cap == NET_CAPABILITY_INTERNET || cap == NET_CAPABILITY_DUN) { + assertEquals(isMobileDataEnabled, vcnCaps.hasCapability(cap)); + } else { + assertTrue(vcnCaps.hasCapability(cap)); + } + } + assertArrayEquals(new int[] {TEST_UID}, vcnCaps.getAdministratorUids()); assertTrue(vcnCaps.getTransportInfo() instanceof VcnTransportInfo); @@ -126,12 +141,17 @@ public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { @Test public void testBuildNetworkCapabilitiesUnderlyingWifi() throws Exception { - verifyBuildNetworkCapabilitiesCommon(TRANSPORT_WIFI); + verifyBuildNetworkCapabilitiesCommon(TRANSPORT_WIFI, true /* isMobileDataEnabled */); } @Test public void testBuildNetworkCapabilitiesUnderlyingCell() throws Exception { - verifyBuildNetworkCapabilitiesCommon(TRANSPORT_CELLULAR); + verifyBuildNetworkCapabilitiesCommon(TRANSPORT_CELLULAR, true /* isMobileDataEnabled */); + } + + @Test + public void testBuildNetworkCapabilitiesMobileDataDisabled() throws Exception { + verifyBuildNetworkCapabilitiesCommon(TRANSPORT_CELLULAR, false /* isMobileDataEnabled */); } @Test diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 98d553dab01a..284f1f88658e 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -202,6 +202,7 @@ public class VcnGatewayConnectionTestBase { TEST_SUBSCRIPTION_SNAPSHOT, mConfig, mGatewayStatusCallback, + true /* isMobileDataEnabled */, mDeps); } diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java index 90eb75e865e7..7c2a5a1cfc5d 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java @@ -16,16 +16,24 @@ package com.android.server.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE; +import static com.android.server.vcn.Vcn.VcnContentResolver; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -35,12 +43,16 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.database.ContentObserver; import android.net.NetworkRequest; +import android.net.Uri; import android.net.vcn.VcnConfig; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; import android.os.ParcelUuid; import android.os.test.TestLooper; +import android.provider.Settings; +import android.telephony.TelephonyManager; import android.util.ArraySet; import com.android.server.VcnManagementService.VcnCallback; @@ -53,7 +65,10 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.UUID; @@ -62,14 +77,21 @@ public class VcnTest { private static final ParcelUuid TEST_SUB_GROUP = new ParcelUuid(new UUID(0, 0)); private static final int NETWORK_SCORE = 0; private static final int PROVIDER_ID = 5; + private static final boolean MOBILE_DATA_ENABLED = true; + private static final Set<Integer> TEST_SUB_IDS_IN_GROUP = + new ArraySet<>(Arrays.asList(1, 2, 3)); private static final int[][] TEST_CAPS = new int[][] { - new int[] {NET_CAPABILITY_MMS, NET_CAPABILITY_INTERNET}, - new int[] {NET_CAPABILITY_DUN} + new int[] {NET_CAPABILITY_IMS, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN}, + new int[] {NET_CAPABILITY_CBS, NET_CAPABILITY_INTERNET}, + new int[] {NET_CAPABILITY_FOTA, NET_CAPABILITY_DUN}, + new int[] {NET_CAPABILITY_MMS} }; private Context mContext; private VcnContext mVcnContext; + private TelephonyManager mTelephonyManager; + private VcnContentResolver mContentResolver; private TelephonySubscriptionSnapshot mSubscriptionSnapshot; private VcnNetworkProvider mVcnNetworkProvider; private VcnCallback mVcnCallback; @@ -86,6 +108,9 @@ public class VcnTest { public void setUp() { mContext = mock(Context.class); mVcnContext = mock(VcnContext.class); + mTelephonyManager = + setupAndGetTelephonyManager(MOBILE_DATA_ENABLED /* isMobileDataEnabled */); + mContentResolver = mock(VcnContentResolver.class); mSubscriptionSnapshot = mock(TelephonySubscriptionSnapshot.class); mVcnNetworkProvider = mock(VcnNetworkProvider.class); mVcnCallback = mock(VcnCallback.class); @@ -97,12 +122,15 @@ public class VcnTest { doReturn(mContext).when(mVcnContext).getContext(); doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper(); doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider(); + doReturn(mContentResolver).when(mDeps).newVcnContentResolver(eq(mVcnContext)); // Setup VcnGatewayConnection instance generation doAnswer((invocation) -> { // Mock-within a doAnswer is safe, because it doesn't actually run nested. return mock(VcnGatewayConnection.class); - }).when(mDeps).newVcnGatewayConnection(any(), any(), any(), any(), any()); + }).when(mDeps).newVcnGatewayConnection(any(), any(), any(), any(), any(), anyBoolean()); + + doReturn(TEST_SUB_IDS_IN_GROUP).when(mSubscriptionSnapshot).getAllSubIdsInGroup(any()); mGatewayStatusCallbackCaptor = ArgumentCaptor.forClass(VcnGatewayStatusCallback.class); @@ -123,6 +151,16 @@ public class VcnTest { mDeps); } + private TelephonyManager setupAndGetTelephonyManager(boolean isMobileDataEnabled) { + final TelephonyManager telephonyManager = mock(TelephonyManager.class); + VcnTestUtils.setupSystemService( + mContext, telephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class); + doReturn(telephonyManager).when(telephonyManager).createForSubscriptionId(anyInt()); + doReturn(isMobileDataEnabled).when(telephonyManager).isDataEnabled(); + + return telephonyManager; + } + private NetworkRequestListener verifyAndGetRequestListener() { ArgumentCaptor<NetworkRequestListener> mNetworkRequestListenerCaptor = ArgumentCaptor.forClass(NetworkRequestListener.class); @@ -164,6 +202,39 @@ public class VcnTest { } @Test + public void testContentObserverRegistered() { + // Validate state from setUp() + final Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + verify(mContentResolver) + .registerContentObserver(eq(uri), eq(true), any(ContentObserver.class)); + } + + @Test + public void testMobileDataStateCheckedOnInitialization_enabled() { + // Validate state from setUp() + assertTrue(mVcn.isMobileDataEnabled()); + verify(mTelephonyManager).isDataEnabled(); + } + + @Test + public void testMobileDataStateCheckedOnInitialization_disabled() { + // Build and setup new telephonyManager to ensure method call count is reset. + final TelephonyManager telephonyManager = + setupAndGetTelephonyManager(false /* isMobileDataEnabled */); + final Vcn vcn = + new Vcn( + mVcnContext, + TEST_SUB_GROUP, + mConfig, + mSubscriptionSnapshot, + mVcnCallback, + mDeps); + + assertFalse(vcn.isMobileDataEnabled()); + verify(mTelephonyManager).isDataEnabled(); + } + + @Test public void testSubscriptionSnapshotUpdatesVcnGatewayConnections() { verifyUpdateSubscriptionSnapshotNotifiesGatewayConnections(VCN_STATUS_CODE_ACTIVE); } @@ -193,7 +264,8 @@ public class VcnTest { eq(TEST_SUB_GROUP), eq(mSubscriptionSnapshot), any(), - mGatewayStatusCallbackCaptor.capture()); + mGatewayStatusCallbackCaptor.capture(), + eq(MOBILE_DATA_ENABLED)); return gatewayConnections; } @@ -256,20 +328,21 @@ public class VcnTest { mTestLooper.dispatchAll(); // Verify that the VCN requests the networkRequests be resent - assertEquals(1, mVcn.getVcnGatewayConnections().size()); + assertEquals(gatewayConnections.size() - 1, mVcn.getVcnGatewayConnections().size()); verify(mVcnNetworkProvider).resendAllRequests(requestListener); // Verify that the VcnGatewayConnection is restarted if a request exists for it triggerVcnRequestListeners(requestListener); mTestLooper.dispatchAll(); - assertEquals(2, mVcn.getVcnGatewayConnections().size()); + assertEquals(gatewayConnections.size(), mVcn.getVcnGatewayConnections().size()); verify(mDeps, times(gatewayConnections.size() + 1)) .newVcnGatewayConnection( eq(mVcnContext), eq(TEST_SUB_GROUP), eq(mSubscriptionSnapshot), any(), - mGatewayStatusCallbackCaptor.capture()); + mGatewayStatusCallbackCaptor.capture(), + anyBoolean()); } @Test @@ -286,7 +359,7 @@ public class VcnTest { public void testUpdateConfigReevaluatesGatewayConnections() { final NetworkRequestListener requestListener = verifyAndGetRequestListener(); startGatewaysAndGetGatewayConnections(requestListener); - assertEquals(2, mVcn.getVcnGatewayConnectionConfigMap().size()); + assertEquals(TEST_CAPS.length, mVcn.getVcnGatewayConnectionConfigMap().size()); // Create VcnConfig with only one VcnGatewayConnectionConfig so a gateway connection is torn // down. Reuse existing VcnGatewayConnectionConfig so that the gateway connection name @@ -309,4 +382,57 @@ public class VcnTest { verify(removedGatewayConnection).teardownAsynchronously(); verify(mVcnNetworkProvider).resendAllRequests(requestListener); } + + private void verifyMobileDataToggled(boolean startingToggleState, boolean endingToggleState) { + final ArgumentCaptor<ContentObserver> captor = + ArgumentCaptor.forClass(ContentObserver.class); + verify(mContentResolver).registerContentObserver(any(), anyBoolean(), captor.capture()); + final ContentObserver contentObserver = captor.getValue(); + + // Start VcnGatewayConnections + mVcn.setMobileDataEnabled(startingToggleState); + triggerVcnRequestListeners(verifyAndGetRequestListener()); + final Map<VcnGatewayConnectionConfig, VcnGatewayConnection> gateways = + mVcn.getVcnGatewayConnectionConfigMap(); + + // Trigger data toggle change. + doReturn(endingToggleState).when(mTelephonyManager).isDataEnabled(); + contentObserver.onChange(false /* selfChange, ignored */); + mTestLooper.dispatchAll(); + + // Verify that data toggle changes restart ONLY INTERNET or DUN networks, and only if the + // toggle state changed. + for (Entry<VcnGatewayConnectionConfig, VcnGatewayConnection> entry : gateways.entrySet()) { + final Set<Integer> exposedCaps = entry.getKey().getAllExposedCapabilities(); + if (startingToggleState != endingToggleState + && (exposedCaps.contains(NET_CAPABILITY_INTERNET) + || exposedCaps.contains(NET_CAPABILITY_DUN))) { + verify(entry.getValue()).teardownAsynchronously(); + } else { + verify(entry.getValue(), never()).teardownAsynchronously(); + } + } + + assertEquals(endingToggleState, mVcn.isMobileDataEnabled()); + } + + @Test + public void testMobileDataEnabled() { + verifyMobileDataToggled(false /* startingToggleState */, true /* endingToggleState */); + } + + @Test + public void testMobileDataDisabled() { + verifyMobileDataToggled(true /* startingToggleState */, false /* endingToggleState */); + } + + @Test + public void testMobileDataObserverFiredWithoutChanges_dataEnabled() { + verifyMobileDataToggled(false /* startingToggleState */, false /* endingToggleState */); + } + + @Test + public void testMobileDataObserverFiredWithoutChanges_dataDisabled() { + verifyMobileDataToggled(true /* startingToggleState */, true /* endingToggleState */); + } } |