Pull VcnNetworkProvider out into a separate class
This change makes the VcnNetworkProvider a separate class, and caches
all NetworkRequest(s) to ensure that VcnTunnel(s) satisfy all requests
that they can accept.
Bug: 163431879
Test: atest FrameworksVcnTests
Change-Id: I3b7695628d0153a33f7e7f40d839df1463d58b07
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 77e8e96..c191a78 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -25,8 +25,6 @@
import android.app.AppOpsManager;
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.NetworkProvider;
-import android.net.NetworkRequest;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.VcnConfig;
import android.os.Binder;
@@ -50,6 +48,7 @@
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import java.io.IOException;
@@ -390,6 +389,9 @@
private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
Slog.v(TAG, "Starting VCN config for subGrp: " + subscriptionGroup);
+ // TODO(b/176939047): Support multiple VCNs active at the same time, or limit to one active
+ // VCN.
+
final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config);
mVcns.put(subscriptionGroup, newInstance);
}
@@ -493,24 +495,4 @@
return Collections.unmodifiableMap(mVcns);
}
}
-
- /**
- * Network provider for VCN networks.
- *
- * @hide
- */
- public class VcnNetworkProvider extends NetworkProvider {
- VcnNetworkProvider(Context context, Looper looper) {
- super(context, looper, VcnNetworkProvider.class.getSimpleName());
- }
-
- @Override
- public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
- synchronized (mLock) {
- for (Vcn instance : mVcns.values()) {
- instance.onNetworkRequested(request, score, providerId);
- }
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java
index 493761b..9d21b92 100644
--- a/services/core/java/com/android/server/vcn/Vcn.java
+++ b/services/core/java/com/android/server/vcn/Vcn.java
@@ -16,32 +16,69 @@
package com.android.server.vcn;
+
import android.annotation.NonNull;
+import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.vcn.VcnConfig;
+import android.net.vcn.VcnGatewayConnectionConfig;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
+import android.util.Slog;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
/**
* Represents an single instance of a VCN.
*
- * <p>Each Vcn instance manages all tunnels for a given subscription group, including per-capability
- * networks, network selection, and multi-homing.
+ * <p>Each Vcn instance manages all {@link VcnGatewayConnection}(s) for a given subscription group,
+ * including per-capability networks, network selection, and multi-homing.
*
* @hide
*/
public class Vcn extends Handler {
private static final String TAG = Vcn.class.getSimpleName();
+ private static final int MSG_EVENT_BASE = 0;
+ private static final int MSG_CMD_BASE = 100;
+
+ /**
+ * A carrier app updated the configuration.
+ *
+ * <p>Triggers update of config, re-evaluating all active and underlying networks.
+ *
+ * @param obj VcnConfig
+ */
+ private static final int MSG_EVENT_CONFIG_UPDATED = MSG_EVENT_BASE;
+
+ /**
+ * A NetworkRequest was added or updated.
+ *
+ * <p>Triggers an evaluation of all active networks, bringing up a new one if necessary.
+ *
+ * @param obj NetworkRequest
+ */
+ private static final int MSG_EVENT_NETWORK_REQUESTED = MSG_EVENT_BASE + 1;
+
+ /** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */
+ private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE;
+
@NonNull private final VcnContext mVcnContext;
@NonNull private final ParcelUuid mSubscriptionGroup;
@NonNull private final Dependencies mDeps;
+ @NonNull private final VcnNetworkRequestListener mRequestListener;
+
+ @NonNull
+ private final Map<VcnGatewayConnectionConfig, VcnGatewayConnection> mVcnGatewayConnections =
+ new HashMap<>();
@NonNull private VcnConfig mConfig;
+ private boolean mIsRunning = true;
+
public Vcn(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@@ -58,31 +95,123 @@
mVcnContext = vcnContext;
mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
mDeps = Objects.requireNonNull(deps, "Missing deps");
+ mRequestListener = new VcnNetworkRequestListener();
mConfig = Objects.requireNonNull(config, "Missing config");
+
+ // Register to receive cached and future NetworkRequests
+ mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener);
}
/** Asynchronously updates the configuration and triggers a re-evaluation of Networks */
public void updateConfig(@NonNull VcnConfig config) {
Objects.requireNonNull(config, "Missing config");
- // TODO: Proxy to handler, and make config there.
+
+ sendMessage(obtainMessage(MSG_EVENT_CONFIG_UPDATED, config));
}
- /** Asynchronously tears down this Vcn instance, along with all tunnels and Networks */
+ /** Asynchronously tears down this Vcn instance, including VcnGatewayConnection(s) */
public void teardownAsynchronously() {
- // TODO: Proxy to handler, and teardown there.
+ sendMessageAtFrontOfQueue(obtainMessage(MSG_CMD_TEARDOWN));
}
- /** Notifies this Vcn instance of a new NetworkRequest */
- public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
- Objects.requireNonNull(request, "Missing request");
+ private class VcnNetworkRequestListener implements VcnNetworkProvider.NetworkRequestListener {
+ @Override
+ public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
+ Objects.requireNonNull(request, "Missing request");
- // TODO: Proxy to handler, and handle there.
+ sendMessage(obtainMessage(MSG_EVENT_NETWORK_REQUESTED, score, providerId, request));
+ }
}
@Override
public void handleMessage(@NonNull Message msg) {
- // TODO: Do something
+ if (!mIsRunning) {
+ return;
+ }
+
+ switch (msg.what) {
+ case MSG_EVENT_CONFIG_UPDATED:
+ handleConfigUpdated((VcnConfig) msg.obj);
+ break;
+ case MSG_EVENT_NETWORK_REQUESTED:
+ handleNetworkRequested((NetworkRequest) msg.obj, msg.arg1, msg.arg2);
+ break;
+ case MSG_CMD_TEARDOWN:
+ handleTeardown();
+ break;
+ default:
+ Slog.wtf(getLogTag(), "Unknown msg.what: " + msg.what);
+ }
+ }
+
+ private void handleConfigUpdated(@NonNull VcnConfig config) {
+ // TODO: Add a dump function in VcnConfig that omits PII. Until then, use hashCode()
+ Slog.v(getLogTag(), String.format("Config updated: config = %s", config.hashCode()));
+
+ mConfig = config;
+
+ // TODO: Reevaluate active VcnGatewayConnection(s)
+ }
+
+ private void handleTeardown() {
+ mVcnContext.getVcnNetworkProvider().unregisterListener(mRequestListener);
+
+ for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
+ gatewayConnection.teardownAsynchronously();
+ }
+
+ mIsRunning = false;
+ }
+
+ private void handleNetworkRequested(
+ @NonNull NetworkRequest request, int score, int providerId) {
+ if (score > getNetworkScore()) {
+ Slog.v(getLogTag(),
+ "Request " + request.requestId + " already satisfied by higher-scoring ("
+ + score + ") network from provider " + providerId);
+ return;
+ }
+
+ // If preexisting VcnGatewayConnection(s) satisfy request, return
+ for (VcnGatewayConnectionConfig gatewayConnectionConfig : mVcnGatewayConnections.keySet()) {
+ if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
+ Slog.v(getLogTag(),
+ "Request " + request.requestId
+ + " satisfied by existing VcnGatewayConnection");
+ return;
+ }
+ }
+
+ // If any supported (but not running) VcnGatewayConnection(s) can satisfy request, bring it
+ // up
+ for (VcnGatewayConnectionConfig gatewayConnectionConfig :
+ mConfig.getGatewayConnectionConfigs()) {
+ if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
+ Slog.v(
+ getLogTag(),
+ "Bringing up new VcnGatewayConnection for request " + request.requestId);
+
+ final VcnGatewayConnection vcnGatewayConnection =
+ new VcnGatewayConnection(
+ mVcnContext, mSubscriptionGroup, gatewayConnectionConfig);
+ mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection);
+ }
+ }
+ }
+
+ private boolean requestSatisfiedByGatewayConnectionConfig(
+ @NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) {
+ final NetworkCapabilities configCaps = new NetworkCapabilities();
+ for (int cap : config.getAllExposedCapabilities()) {
+ configCaps.addCapability(cap);
+ }
+
+ return request.networkCapabilities.satisfiedByNetworkCapabilities(configCaps);
+ }
+
+ private String getLogTag() {
+ return String.format("%s [%d]", TAG, mSubscriptionGroup.hashCode());
}
/** Retrieves the network score for a VCN Network */
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 8ab52931..dba59bd 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -20,8 +20,6 @@
import android.content.Context;
import android.os.Looper;
-import com.android.server.VcnManagementService.VcnNetworkProvider;
-
import java.util.Objects;
/**
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 49c9b32..7ea8e04 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -65,8 +65,8 @@
mDeps.newUnderlyingNetworkTracker(mVcnContext, subscriptionGroup, this);
}
- /** Tears down this GatewayConnection, and any resources used */
- public void teardown() {
+ /** Asynchronously tears down this GatewayConnection, and any resources used */
+ public void teardownAsynchronously() {
mUnderlyingNetworkTracker.teardown();
}
diff --git a/services/core/java/com/android/server/vcn/VcnNetworkProvider.java b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java
new file mode 100644
index 0000000..7f5b23c
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 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.server.vcn;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.os.Looper;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * VCN Network Provider routes NetworkRequests to listeners to bring up tunnels as needed.
+ *
+ * <p>The VcnNetworkProvider provides a caching layer to ensure that all listeners receive all
+ * active NetworkRequest(s), including ones that were filed prior to listener registration.
+ *
+ * @hide
+ */
+public class VcnNetworkProvider extends NetworkProvider {
+ private static final String TAG = VcnNetworkProvider.class.getSimpleName();
+
+ private final Set<NetworkRequestListener> mListeners = new ArraySet<>();
+ private final SparseArray<NetworkRequestEntry> mRequests = new SparseArray<>();
+
+ public VcnNetworkProvider(Context context, Looper looper) {
+ super(context, looper, VcnNetworkProvider.class.getSimpleName());
+ }
+
+ // Package-private
+ void registerListener(@NonNull NetworkRequestListener listener) {
+ mListeners.add(listener);
+
+ // Send listener all cached requests
+ for (int i = 0; i < mRequests.size(); i++) {
+ notifyListenerForEvent(listener, mRequests.valueAt(i));
+ }
+ }
+
+ // Package-private
+ void unregisterListener(@NonNull NetworkRequestListener listener) {
+ mListeners.remove(listener);
+ }
+
+ private void notifyListenerForEvent(
+ @NonNull NetworkRequestListener listener, @NonNull NetworkRequestEntry entry) {
+ listener.onNetworkRequested(entry.mRequest, entry.mScore, entry.mProviderId);
+ }
+
+ @Override
+ public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
+ Slog.v(
+ TAG,
+ String.format(
+ "Network requested: Request = %s, score = %d, providerId = %d",
+ request, score, providerId));
+
+ final NetworkRequestEntry entry = new NetworkRequestEntry(request, score, providerId);
+ mRequests.put(request.requestId, entry);
+
+ // TODO(b/176939047): Intelligently route requests to prioritized VcnInstances (based on
+ // Default Data Sub, or similar)
+ for (NetworkRequestListener listener : mListeners) {
+ notifyListenerForEvent(listener, entry);
+ }
+ }
+
+ @Override
+ public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
+ mRequests.remove(request.requestId);
+ }
+
+ private static class NetworkRequestEntry {
+ public final NetworkRequest mRequest;
+ public final int mScore;
+ public final int mProviderId;
+
+ private NetworkRequestEntry(@NonNull NetworkRequest request, int score, int providerId) {
+ mRequest = Objects.requireNonNull(request, "Missing request");
+ mScore = score;
+ mProviderId = providerId;
+ }
+ }
+
+ // package-private
+ interface NetworkRequestListener {
+ void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId);
+ }
+}
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index d2caa8f..696110f 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -49,10 +49,10 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.server.VcnManagementService.VcnNetworkProvider;
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import org.junit.Test;
@@ -191,8 +191,7 @@
public void testSystemReady() throws Exception {
mVcnMgmtSvc.systemReady();
- verify(mConnMgr)
- .registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
+ verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class));
verify(mSubscriptionTracker).register();
}
@@ -309,7 +308,7 @@
// Config cleared, SIM reloaded & config re-added right before teardown delay, staring new
// vcnInstance.
mTestLooper.moveTimeForward(
- VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS - 1);
+ VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
mTestLooper.dispatchAll();
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2);
final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2);
diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
new file mode 100644
index 0000000..c2c6200
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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.server.vcn;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tests for TelephonySubscriptionTracker */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VcnNetworkProviderTest {
+ private static final int TEST_SCORE_UNSATISFIED = 0;
+ private static final int TEST_SCORE_HIGH = 100;
+ private static final int TEST_PROVIDER_ID = 1;
+ private static final int TEST_LEGACY_TYPE = ConnectivityManager.TYPE_MOBILE;
+ private static final NetworkRequest.Type TEST_REQUEST_TYPE = NetworkRequest.Type.REQUEST;
+
+ @NonNull private final Context mContext;
+ @NonNull private final TestLooper mTestLooper;
+
+ @NonNull private VcnNetworkProvider mVcnNetworkProvider;
+ @NonNull private NetworkRequestListener mListener;
+
+ public VcnNetworkProviderTest() {
+ mContext = mock(Context.class);
+ mTestLooper = new TestLooper();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mVcnNetworkProvider = new VcnNetworkProvider(mContext, mTestLooper.getLooper());
+ mListener = mock(NetworkRequestListener.class);
+ }
+
+ @Test
+ public void testRequestsPassedToRegisteredListeners() throws Exception {
+ mVcnNetworkProvider.registerListener(mListener);
+
+ final NetworkRequest request = mock(NetworkRequest.class);
+ mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
+ verify(mListener).onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
+ }
+
+ @Test
+ public void testRequestsPassedToRegisteredListeners_satisfiedByHighScoringProvider()
+ throws Exception {
+ mVcnNetworkProvider.registerListener(mListener);
+
+ final NetworkRequest request = mock(NetworkRequest.class);
+ mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
+ verify(mListener).onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
+ }
+
+ @Test
+ public void testUnregisterListener() throws Exception {
+ mVcnNetworkProvider.registerListener(mListener);
+ mVcnNetworkProvider.unregisterListener(mListener);
+
+ final NetworkRequest request = mock(NetworkRequest.class);
+ mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
+ verifyNoMoreInteractions(mListener);
+ }
+
+ @Test
+ public void testCachedRequestsPassedOnRegister() throws Exception {
+ final List<NetworkRequest> requests = new ArrayList<>();
+
+ for (int i = 0; i < 10; i++) {
+ final NetworkRequest request =
+ new NetworkRequest(
+ new NetworkCapabilities(),
+ TEST_LEGACY_TYPE,
+ i /* requestId */,
+ TEST_REQUEST_TYPE);
+
+ requests.add(request);
+ mVcnNetworkProvider.onNetworkRequested(request, i, i + 1);
+ }
+
+ mVcnNetworkProvider.registerListener(mListener);
+ for (int i = 0; i < requests.size(); i++) {
+ final NetworkRequest request = requests.get(i);
+ verify(mListener).onNetworkRequested(request, i, i + 1);
+ }
+ verifyNoMoreInteractions(mListener);
+ }
+}