Remove the queue on the NetworkAgent side

Now that the system server has a queue doing this job,
the queue on the network agent side can be removed.

Test: ConnectivityCoverageTests, CtsNetTestCases
Change-Id: Idf0435f447cfdd28d829a867d1085771837c6f9b
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 4f18fa2..f8a1293 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1200,11 +1200,14 @@
 
     /** @hide */
     public static final long FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS = 1L;
+    /** @hide */
+    public static final long FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER = 1L << 1;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @LongDef(flag = true, prefix = "FEATURE_", value = {
-            FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS
+            FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS,
+            FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER
     })
     public @interface ConnectivityManagerFeature {}
 
@@ -4881,7 +4884,8 @@
         return 0;
     }
 
-    private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
+    /** @hide */
+    public boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
         synchronized (mEnabledConnectivityManagerFeaturesLock) {
             if (mEnabledConnectivityManagerFeatures == null) {
                 try {
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 08f5ecd..95b45ce 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -37,6 +37,7 @@
 import android.telephony.data.NrQosSessionAttributes;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.FrameworkConnectivityStatsLog;
 
@@ -116,15 +117,33 @@
     private final ArrayList<RegistryAction> mPreConnectedQueue = new ArrayList<>();
     private volatile long mLastBwRefreshTime = 0;
     private static final long BW_REFRESH_MIN_WIN_MS = 500;
+
+    private final boolean mQueueRemoved;
+
     private boolean mBandwidthUpdateScheduled = false;
     private AtomicBoolean mBandwidthUpdatePending = new AtomicBoolean(false);
     @NonNull
     private NetworkInfo mNetworkInfo;
     @NonNull
     private final Object mRegisterLock = new Object();
-    // TODO : move the preconnected queue to the system server and remove this
+    // TODO : when ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER is
+    // not chickened out this is never read. Remove when retiring this flag.
     private boolean mConnected = false;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATE_" }, value = {
+        STATE_CREATED,
+        STATE_REGISTERED,
+        STATE_UNREGISTERED
+    })
+    public @interface NetworkAgentState {}
+    private static final int STATE_CREATED = 0;
+    private static final int STATE_REGISTERED = 1;
+    private static final int STATE_UNREGISTERED = 2;
+    @GuardedBy("mRegisterLock")
+    private int mState = STATE_CREATED;
+
     /**
      * The ID of the {@link NetworkProvider} that created this object, or
      * {@link NetworkProvider#ID_NONE} if unknown.
@@ -506,6 +525,18 @@
         return ni;
     }
 
+    /**
+     * Returns whether a given ConnectivityManager feature is enabled.
+     *
+     * Tests can override this.
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean isFeatureEnabled(@NonNull Context context,
+            @ConnectivityManager.ConnectivityManagerFeature long feature) {
+        return context.getSystemService(ConnectivityManager.class).isFeatureEnabled(feature);
+    }
+
     // Temporary backward compatibility constructor
     public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
             @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp, int score,
@@ -588,6 +619,10 @@
             @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
             @NonNull NetworkAgentConfig config, int providerId, @NonNull NetworkInfo ni) {
         mHandler = new NetworkAgentHandler(looper);
+        // If the feature is enabled, then events are queued in the system
+        // server, and it's removed from this NetworkAgent.
+        mQueueRemoved = isFeatureEnabled(context,
+                ConnectivityManager.FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER);
         LOG_TAG = logTag;
         mNetworkInfo = new NetworkInfo(ni);
         this.providerId = providerId;
@@ -609,24 +644,31 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_AGENT_CONNECTED: {
-                    // TODO : move the pre-connected queue to the system server, and remove
-                    // handling this EVENT_AGENT_CONNECTED message.
-                    synchronized (mPreConnectedQueue) {
-                        if (mConnected) {
-                            log("Received new connection while already connected!");
-                        } else {
-                            if (VDBG) log("NetworkAgent fully connected");
-                            for (RegistryAction a : mPreConnectedQueue) {
-                                try {
-                                    a.execute(mRegistry);
-                                } catch (RemoteException e) {
-                                    Log.wtf(LOG_TAG, "Communication error with registry", e);
-                                    // Fall through
+                    if (mQueueRemoved) {
+                        // No handling. This message is legacy from a time where the
+                        // agent had to wait until the registry was sent to it, which
+                        // would only happen after the corresponding NetworkMonitor
+                        // was created.
+                        mConnected = true; // never read, but mConnected = false would be confusing
+                    } else {
+                        // Feature chickened out, keep the old queueing behavior
+                        synchronized (mRegisterLock) {
+                            if (mConnected) {
+                                log("Received new connection while already connected!");
+                            } else {
+                                if (VDBG) log("NetworkAgent fully connected");
+                                for (RegistryAction a : mPreConnectedQueue) {
+                                    try {
+                                        a.execute(mRegistry);
+                                    } catch (RemoteException e) {
+                                        Log.wtf(LOG_TAG, "Communication error with registry", e);
+                                        // Fall through
+                                    }
                                 }
+                                mPreConnectedQueue.clear();
                             }
-                            mPreConnectedQueue.clear();
+                            mConnected = true;
                         }
-                        mConnected = true;
                     }
                     break;
                 }
@@ -634,7 +676,8 @@
                     if (DBG) log("NetworkAgent channel lost");
                     // let the client know CS is done with us.
                     onNetworkUnwanted();
-                    synchronized (mPreConnectedQueue) {
+                    synchronized (mRegisterLock) {
+                        mState = STATE_UNREGISTERED;
                         mConnected = false;
                     }
                     break;
@@ -757,8 +800,19 @@
     public Network register() {
         if (VDBG) log("Registering NetworkAgent");
         synchronized (mRegisterLock) {
-            if (mNetwork != null) {
-                throw new IllegalStateException("Agent already registered");
+            if (mQueueRemoved) {
+                switch (mState) {
+                    case STATE_REGISTERED:
+                        throw new IllegalStateException("Agent already registered");
+                    case STATE_UNREGISTERED:
+                        throw new IllegalStateException("Agent already unregistered");
+                    default: // CREATED, this is the normal case
+                }
+            } else {
+                // Feature is chickened out, do the old processing
+                if (mNetwork != null) {
+                    throw new IllegalStateException("Agent already registered");
+                }
             }
             final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
                     .getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -787,6 +841,7 @@
             } else {
                 mNetwork = result.network;
                 mRegistry = result.registry;
+                mState = STATE_REGISTERED;
             }
             mInitialConfiguration = null; // All this memory can now be GC'd
         }
@@ -936,6 +991,7 @@
             mNetwork = network;
             mInitialConfiguration = null;
             mRegistry = registry;
+            mState = STATE_REGISTERED;
         }
         return new NetworkAgentBinder(mHandler);
     }
@@ -961,30 +1017,49 @@
         return mNetwork;
     }
 
-    private void queueOrSendMessage(@NonNull RegistryAction action) {
-        synchronized (mPreConnectedQueue) {
-            if (mNetwork == null && !Process.isApplicationUid(Process.myUid())) {
-                // Theoretically, it should not be valid to queue messages here before
-                // registering the NetworkAgent. However, practically, with the way
-                // queueing works right now, it ends up working out just fine.
-                // Log a statistic so that we know if this is happening in the
-                // wild. The check for isApplicationUid is to prevent logging the
-                // metric from test code.
+    private void logTerribleErrorMessageBeforeConnect() {
+        FrameworkConnectivityStatsLog.write(
+                FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_MESSAGE_QUEUED_BEFORE_CONNECT
+        );
+    }
 
-                FrameworkConnectivityStatsLog.write(
-                        FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
-                        FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_MESSAGE_QUEUED_BEFORE_CONNECT
-                );
-            }
-            if (mConnected) {
-                try {
-                    action.execute(mRegistry);
-                } catch (RemoteException e) {
-                    Log.wtf(LOG_TAG, "Error executing registry action", e);
-                    // Fall through: the channel is asynchronous and does not report errors back
+    private void send(@NonNull RegistryAction action) {
+        synchronized (mRegisterLock) {
+            if (mQueueRemoved) {
+                if (mState <= STATE_CREATED) {
+                    // Log a terrible error. There is nothing to do with this message
+                    // so drop it.
+                    logTerribleErrorMessageBeforeConnect();
+                    Log.e(LOG_TAG, "Agent not yet registered, ignoring command");
+                    return;
+                }
+                if (mState >= STATE_UNREGISTERED) {
+                    // This should not crash for two reasons : first, the agent may
+                    // be disconnected by ConnectivityService at any time and the message
+                    // typically arrives on another thread, so it's not feasible for
+                    // apps to check before sending, they'd have to always catch. Second,
+                    // historically this hasn't thrown and some code may be relying on
+                    // the historical behavior.
+                    Log.e(LOG_TAG, "Agent already unregistered, ignoring command");
+                    return;
                 }
             } else {
-                mPreConnectedQueue.add(action);
+                if (null == mNetwork) {
+                    // Log a terrible error but still enqueue the message for backward
+                    // compatibility.
+                    logTerribleErrorMessageBeforeConnect();
+                }
+                if (!mConnected) {
+                    mPreConnectedQueue.add(action);
+                    return;
+                }
+            }
+            try {
+                action.execute(mRegistry);
+            } catch (RemoteException e) {
+                Log.wtf(LOG_TAG, "Error executing registry action", e);
+                // Fall through: the channel is asynchronous and does not report errors back
             }
         }
     }
@@ -995,8 +1070,9 @@
      */
     public void sendLinkProperties(@NonNull LinkProperties linkProperties) {
         Objects.requireNonNull(linkProperties);
-        final LinkProperties lp = new LinkProperties(linkProperties);
-        queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
+        // Copy the object because if the agent is running in the system server
+        // then the same instance will be seen by the registry
+        send(reg -> reg.sendLinkProperties(new LinkProperties(linkProperties)));
     }
 
     /**
@@ -1022,7 +1098,7 @@
             @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
         final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
                 ? new ArrayList<>(underlyingNetworks) : null;
-        queueOrSendMessage(reg -> reg.sendUnderlyingNetworks(underlyingArray));
+        send(reg -> reg.sendUnderlyingNetworks(underlyingArray));
     }
 
     /**
@@ -1032,7 +1108,7 @@
     public void markConnected() {
         mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null /* reason */,
                 mNetworkInfo.getExtraInfo());
-        queueOrSendNetworkInfo(mNetworkInfo);
+        sendNetworkInfo(mNetworkInfo);
     }
 
     /**
@@ -1045,7 +1121,12 @@
         // When unregistering an agent nobody should use the extrainfo (or reason) any more.
         mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, null /* reason */,
                 null /* extraInfo */);
-        queueOrSendNetworkInfo(mNetworkInfo);
+        synchronized (mRegisterLock) {
+            if (mState >= STATE_REGISTERED) {
+                sendNetworkInfo(mNetworkInfo);
+            }
+            mState = STATE_UNREGISTERED;
+        }
     }
 
     /**
@@ -1068,7 +1149,7 @@
      */
     public void setTeardownDelayMillis(
             @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int teardownDelayMillis) {
-        queueOrSendMessage(reg -> reg.sendTeardownDelayMs(teardownDelayMillis));
+        send(reg -> reg.sendTeardownDelayMs(teardownDelayMillis));
     }
 
     /**
@@ -1107,7 +1188,7 @@
      */
     public void unregisterAfterReplacement(
             @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int timeoutMillis) {
-        queueOrSendMessage(reg -> reg.sendUnregisterAfterReplacement(timeoutMillis));
+        send(reg -> reg.sendUnregisterAfterReplacement(timeoutMillis));
     }
 
     /**
@@ -1125,7 +1206,7 @@
     @SystemApi
     public void setLegacySubtype(final int legacySubtype, @NonNull final String legacySubtypeName) {
         mNetworkInfo.setSubtype(legacySubtype, legacySubtypeName);
-        queueOrSendNetworkInfo(mNetworkInfo);
+        sendNetworkInfo(mNetworkInfo);
     }
 
     /**
@@ -1147,7 +1228,7 @@
     @Deprecated
     public void setLegacyExtraInfo(@Nullable final String extraInfo) {
         mNetworkInfo.setExtraInfo(extraInfo);
-        queueOrSendNetworkInfo(mNetworkInfo);
+        sendNetworkInfo(mNetworkInfo);
     }
 
     /**
@@ -1155,13 +1236,9 @@
      * @hide TODO: expose something better.
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
-    public final void sendNetworkInfo(NetworkInfo networkInfo) {
-        queueOrSendNetworkInfo(networkInfo);
-    }
-
-    private void queueOrSendNetworkInfo(NetworkInfo networkInfo) {
+    private void sendNetworkInfo(final NetworkInfo networkInfo) {
         final NetworkInfo ni = new NetworkInfo(networkInfo);
-        queueOrSendMessage(reg -> reg.sendNetworkInfo(ni));
+        send(reg -> reg.sendNetworkInfo(ni));
     }
 
     /**
@@ -1174,7 +1251,7 @@
         mLastBwRefreshTime = System.currentTimeMillis();
         final NetworkCapabilities nc =
                 new NetworkCapabilities(networkCapabilities, NetworkCapabilities.REDACT_NONE);
-        queueOrSendMessage(reg -> reg.sendNetworkCapabilities(nc));
+        send(reg -> reg.sendNetworkCapabilities(nc));
     }
 
     /**
@@ -1186,7 +1263,7 @@
         Objects.requireNonNull(config);
         // If the agent doesn't have NET_CAPABILITY_LOCAL_NETWORK, this will be ignored by
         // ConnectivityService with a Log.wtf.
-        queueOrSendMessage(reg -> reg.sendLocalNetworkConfig(config));
+        send(reg -> reg.sendLocalNetworkConfig(config));
     }
 
     /**
@@ -1196,7 +1273,7 @@
      */
     public void sendNetworkScore(@NonNull NetworkScore score) {
         Objects.requireNonNull(score);
-        queueOrSendMessage(reg -> reg.sendScore(score));
+        send(reg -> reg.sendScore(score));
     }
 
     /**
@@ -1246,8 +1323,7 @@
      * @hide should move to NetworkAgentConfig.
      */
     public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) {
-        queueOrSendMessage(reg -> reg.sendExplicitlySelected(
-                explicitlySelected, acceptUnvalidated));
+        send(reg -> reg.sendExplicitlySelected(explicitlySelected, acceptUnvalidated));
     }
 
     /**
@@ -1387,7 +1463,7 @@
      */
     public final void sendSocketKeepaliveEvent(int slot,
             @SocketKeepalive.KeepaliveEvent int event) {
-        queueOrSendMessage(reg -> reg.sendSocketKeepaliveEvent(slot, event));
+        send(reg -> reg.sendSocketKeepaliveEvent(slot, event));
     }
     /** @hide TODO delete once callers have moved to sendSocketKeepaliveEvent */
     public void onSocketKeepaliveEvent(int slot, int reason) {
@@ -1493,11 +1569,11 @@
             @NonNull final QosSessionAttributes attributes) {
         Objects.requireNonNull(attributes, "The attributes must be non-null");
         if (attributes instanceof EpsBearerQosSessionAttributes) {
-            queueOrSendMessage(ra -> ra.sendEpsQosSessionAvailable(qosCallbackId,
+            send(reg -> reg.sendEpsQosSessionAvailable(qosCallbackId,
                     new QosSession(sessionId, QosSession.TYPE_EPS_BEARER),
                     (EpsBearerQosSessionAttributes)attributes));
         } else if (attributes instanceof NrQosSessionAttributes) {
-            queueOrSendMessage(ra -> ra.sendNrQosSessionAvailable(qosCallbackId,
+            send(reg -> reg.sendNrQosSessionAvailable(qosCallbackId,
                     new QosSession(sessionId, QosSession.TYPE_NR_BEARER),
                     (NrQosSessionAttributes)attributes));
         }
@@ -1512,7 +1588,7 @@
      */
     public final void sendQosSessionLost(final int qosCallbackId,
             final int sessionId, final int qosSessionType) {
-        queueOrSendMessage(ra -> ra.sendQosSessionLost(qosCallbackId,
+        send(reg -> reg.sendQosSessionLost(qosCallbackId,
                 new QosSession(sessionId, qosSessionType)));
     }
 
@@ -1526,7 +1602,7 @@
      */
     public final void sendQosCallbackError(final int qosCallbackId,
             @QosCallbackException.ExceptionType final int exceptionType) {
-        queueOrSendMessage(ra -> ra.sendQosCallbackError(qosCallbackId, exceptionType));
+        send(reg -> reg.sendQosCallbackError(qosCallbackId, exceptionType));
     }
 
     /**
@@ -1543,7 +1619,7 @@
             throw new IllegalArgumentException("Duration must be within ["
                     + MIN_LINGER_TIMER_MS + "," + Integer.MAX_VALUE + "]ms");
         }
-        queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
+        send(reg -> reg.sendLingerDuration((int) durationMs));
     }
 
     /**
@@ -1552,7 +1628,7 @@
      */
     public void sendAddDscpPolicy(@NonNull final DscpPolicy policy) {
         Objects.requireNonNull(policy);
-        queueOrSendMessage(ra -> ra.sendAddDscpPolicy(policy));
+        send(reg -> reg.sendAddDscpPolicy(policy));
     }
 
     /**
@@ -1560,14 +1636,14 @@
      * @param policyId the ID corresponding to a specific DSCP Policy.
      */
     public void sendRemoveDscpPolicy(final int policyId) {
-        queueOrSendMessage(ra -> ra.sendRemoveDscpPolicy(policyId));
+        send(reg -> reg.sendRemoveDscpPolicy(policyId));
     }
 
     /**
      * Remove all the DSCP policies on this network.
      */
     public void sendRemoveAllDscpPolicies() {
-        queueOrSendMessage(ra -> ra.sendRemoveAllDscpPolicies());
+        send(reg -> reg.sendRemoveAllDscpPolicies());
     }
 
     /** @hide */
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 329b338..69d83e8 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -158,6 +158,7 @@
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
 import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
 import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
+import static com.android.server.connectivity.ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER;
 import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
 
@@ -9418,10 +9419,11 @@
         if (DBG) log("registerNetworkAgent " + nai);
         mDeps.getNetworkStack().makeNetworkMonitor(
                 nai.network, name, new NetworkMonitorCallbacks(nai));
-        // NetworkAgentInfo registration will finish when the NetworkMonitor is created.
-        // If the network disconnects or sends any other event before that, messages are deferred by
-        // NetworkAgent until nai.connect(), which will be called when finalizing the
-        // registration. TODO : have NetworkAgentInfo defer them instead.
+        // NetworkAgentInfo registration is done, but CS will only accept messages when the
+        // NetworkMonitor is created. If the network disconnects or sends any other event
+        // before that, messages are deferred by the Tracker Handler until it is (by asking
+        // NetworkAgentInfo to do it). The window is very small unless the NetworkStack
+        // doesn't reply immediately, which would mean a broken system anyway.
         final NetworkAndAgentRegistryParcelable result = new NetworkAndAgentRegistryParcelable();
         result.network = nai.network;
         result.registry = nai.getRegistry();
@@ -15048,6 +15050,9 @@
         if (mUseDeclaredMethodsForCallbacksEnabled) {
             features |= ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS;
         }
+        if (mQueueNetworkAgentEventsInSystemServer) {
+            features |= ConnectivityManager.FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER;
+        }
         return features;
     }
 
@@ -15056,6 +15061,8 @@
         switch (featureFlag) {
             case INGRESS_TO_VPN_ADDRESS_FILTERING:
                 return mIngressToVpnAddressFiltering;
+            case QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER:
+                return mQueueNetworkAgentEventsInSystemServer;
             default:
                 throw new IllegalArgumentException("Unknown flag: " + featureFlag);
         }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index bfbbc34..0413ed4 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -17,6 +17,7 @@
 package com.android.testutils
 
 import android.content.Context
+import android.net.ConnectivityManager.FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER
 import android.net.InetAddresses.parseNumericAddress
 import android.net.KeepalivePacketData
 import android.net.LinkAddress
@@ -28,6 +29,7 @@
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkProvider
 import android.net.NetworkRequest
+import android.net.NetworkScore
 import android.net.QosFilter
 import android.net.Uri
 import android.os.Looper
@@ -64,16 +66,21 @@
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
+import kotlin.test.fail
 import org.junit.Assert.assertArrayEquals
 
 // Any legal score (0~99) for the test network would do, as it is going to be kept up by the
 // requests filed by the test and should never match normal internet requests. 70 is the default
 // score of Ethernet networks, it's as good a value as any other.
-private const val TEST_NETWORK_SCORE = 70
+private val TEST_NETWORK_SCORE = NetworkScore.Builder().setLegacyInt(70).build()
 
 private class Provider(context: Context, looper: Looper) :
             NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
 
+private val enabledFeatures = mutableMapOf(
+    FEATURE_QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER to true
+)
+
 public open class TestableNetworkAgent(
     context: Context,
     looper: Looper,
@@ -81,8 +88,17 @@
     val lp: LinkProperties,
     conf: NetworkAgentConfig
 ) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
-        nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+    nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+
+    override fun isFeatureEnabled(context: Context, feature: Long): Boolean {
+        when (val it = enabledFeatures.get(feature)) {
+            null -> fail("Unmocked feature $feature, see TestableNetworkAgent.enabledFeatures")
+            else -> return it
+        }
+    }
+
     companion object {
+        fun setFeatureEnabled(flag: Long, enabled: Boolean) = enabledFeatures.set(flag, enabled)
 
         /**
          * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 1fa9e3a..bd9bd2a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -384,9 +384,9 @@
             initialLp = lp,
             initialNc = nc
         )
-        agent.setTeardownDelayMillis(0)
         // Connect the agent and verify initial status callbacks.
         agent.register()
+        agent.setTeardownDelayMillis(0)
         agent.markConnected()
         agent.expectCallback<OnNetworkCreated>()
         agent.expectPostConnectionCallbacks(expectedInitSignalStrengthThresholds)
@@ -1934,4 +1934,19 @@
         // VPN networks are always created as soon as the agent is registered.
         doTestNativeNetworkCreation(expectCreatedImmediately = true, intArrayOf(TRANSPORT_VPN))
     }
+
+    @Test(expected = IllegalStateException::class)
+    fun testRegisterAgain() {
+        val agent = createNetworkAgent()
+        agent.register()
+        agent.unregister()
+        agent.register()
+    }
+
+    @Test
+    fun testUnregisterBeforeRegister() {
+        // For backward compatibility, this shouldn't crash.
+        val agent = createNetworkAgent()
+        agent.unregister()
+    }
 }