Add a first CTS for NetworkScore

In an effort to make reviewing easier, this implements most of
the infra but only a mostly trivial test.

Ignore-AOSP-First: NetworkScore incomplete in AOSP
Bug: 184037351
Test: this
Change-Id: Ibb9139798ce44d748e87bae79a1e23311ec8d9b6
diff --git a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
new file mode 100644
index 0000000..2475876
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkCallback.HasNetwork
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// This test doesn't really have a constraint on how fast the methods should return. If it's
+// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
+// without affecting the run time of successful runs. Thus, set a very high timeout.
+private const val TIMEOUT_MS = 30_000L
+// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
+// only possible thing (the relevant handler is the one in the real ConnectivityService,
+// and then there is the Binder call), so have a short timeout for this as it will be
+// exhausted every time.
+private const val NO_CALLBACK_TIMEOUT = 200L
+
+private val testContext: Context
+    get() = InstrumentationRegistry.getContext()
+
+private fun score(exiting: Boolean = false, primary: Boolean = false) =
+        NetworkScore.Builder().setExiting(exiting).setTransportPrimary(primary)
+                // TODO : have a constant KEEP_CONNECTED_FOR_TEST ?
+                .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_FOR_HANDOVER)
+                .build()
+
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkScoreTest {
+    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)
+    private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+    private val mHandler by lazy { Handler(mHandlerThread.looper) }
+    private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+    private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+    @Before
+    fun setUp() {
+        mHandlerThread.start()
+    }
+
+    @After
+    fun tearDown() {
+        agentsToCleanUp.forEach { it.unregister() }
+        mHandlerThread.quitSafely()
+        callbacksToCleanUp.forEach { mCm.unregisterNetworkCallback(it) }
+    }
+
+    // Returns a networkCallback that sends onAvailable on the best network with TRANSPORT_TEST.
+    private fun makeTestNetworkCallback() = TestableNetworkCallback(TIMEOUT_MS).also { cb ->
+        mCm.registerBestMatchingNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST).build(), cb, mHandler)
+        callbacksToCleanUp.add(cb)
+    }
+
+    // TestNetworkCallback is made to interact with a wrapper of NetworkAgent, because it's
+    // made for ConnectivityServiceTest.
+    // TODO : have TestNetworkCallback work for NetworkAgent too and remove this class.
+    private class AgentWrapper(val agent: NetworkAgent) : HasNetwork {
+        override val network = agent.network
+        fun sendNetworkScore(s: NetworkScore) = agent.sendNetworkScore(s)
+    }
+
+    private fun createTestNetworkAgent(
+            // The network always has TRANSPORT_TEST, plus optional transports
+        optionalTransports: IntArray = IntArray(size = 0),
+        everUserSelected: Boolean = false,
+        acceptUnvalidated: Boolean = false,
+        isExiting: Boolean = false,
+        isPrimary: Boolean = false
+    ): AgentWrapper {
+        val nc = NetworkCapabilities.Builder().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            optionalTransports.forEach { addTransportType(it) }
+            // Add capabilities that are common, just for realism. It's not strictly necessary
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+            // Remove capabilities that a test network agent shouldn't have and that are not
+            // needed for the purposes of this test.
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+            if (optionalTransports.contains(NetworkCapabilities.TRANSPORT_VPN)) {
+                addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+            }
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+        }.build()
+        val config = NetworkAgentConfig.Builder()
+                .setExplicitlySelected(everUserSelected)
+                .setUnvalidatedConnectivityAcceptable(acceptUnvalidated)
+                .build()
+        val score = score(exiting = isExiting, primary = isPrimary)
+        val context = testContext
+        val looper = mHandlerThread.looper
+        val agent = object : NetworkAgent(context, looper, "NetworkScore test agent", nc,
+                LinkProperties(), score, config, NetworkProvider(context, looper,
+                "NetworkScore test provider")) {}.also {
+            agentsToCleanUp.add(it)
+        }
+        runWithShellPermissionIdentity({ agent.register() }, MANAGE_TEST_NETWORKS)
+        agent.markConnected()
+        return AgentWrapper(agent)
+    }
+
+    @Test
+    fun testExitingLosesAndOldSatisfierWins() {
+        val cb = makeTestNetworkCallback()
+        val agent1 = createTestNetworkAgent()
+        cb.expectAvailableThenValidatedCallbacks(agent1)
+        val agent2 = createTestNetworkAgent()
+        // Because the existing network must win, the callback stays on agent1.
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        agent1.sendNetworkScore(score(exiting = true))
+        // Now that agent1 is exiting, the callback is satisfied by agent2.
+        cb.expectAvailableCallbacks(agent2.network)
+        agent1.sendNetworkScore(score(exiting = false))
+        // Agent1 is no longer exiting, but agent2 is the current satisfier.
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+    }
+}