Merge "Fix error message in RestrictBackgroundNetworkTest" into rvc-dev
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 76bb27e..46fae33 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -47,6 +47,7 @@
         "mockwebserver",
         "junit",
         "junit-params",
+        "libnanohttpd",
         "truth-prebuilt",
     ],
 
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
new file mode 100644
index 0000000..4418e17
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -0,0 +1,254 @@
+/*
+ * 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 android.net.cts
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.Uri
+import android.net.cts.util.CtsNetUtils
+import android.net.wifi.WifiManager
+import android.os.ConditionVariable
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.text.TextUtils
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil
+import fi.iki.elonen.NanoHTTPD
+import fi.iki.elonen.NanoHTTPD.Response.IStatus
+import fi.iki.elonen.NanoHTTPD.Response.Status
+import junit.framework.AssertionFailedError
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.test.Test
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+private const val TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING = "test_captive_portal_https_url"
+private const val TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING = "test_captive_portal_http_url"
+private const val TEST_URL_EXPIRATION_TIME = "test_url_expiration_time"
+
+private const val TEST_HTTPS_URL_PATH = "https_path"
+private const val TEST_HTTP_URL_PATH = "http_path"
+private const val TEST_PORTAL_URL_PATH = "portal_path"
+
+private const val LOCALHOST_HOSTNAME = "localhost"
+
+// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
+private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L
+private const val TEST_TIMEOUT_MS = 10_000L
+
+private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
+    try {
+        return get(timeoutMs, TimeUnit.MILLISECONDS)
+    } catch (e: TimeoutException) {
+        throw AssertionFailedError(message)
+    }
+}
+
+@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+@RunWith(AndroidJUnit4::class)
+class CaptivePortalTest {
+    private val context: android.content.Context by lazy { getInstrumentation().context }
+    private val wm by lazy { context.getSystemService(WifiManager::class.java) }
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val pm by lazy { context.packageManager }
+    private val utils by lazy { CtsNetUtils(context) }
+
+    private val server = HttpServer()
+
+    @Before
+    fun setUp() {
+        doAsShell(READ_DEVICE_CONFIG) {
+            // Verify that the test URLs are not normally set on the device, but do not fail if the
+            // test URLs are set to what this test uses (URLs on localhost), in case the test was
+            // interrupted manually and rerun.
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING)
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING)
+        }
+        clearTestUrls()
+        server.start()
+    }
+
+    @After
+    fun tearDown() {
+        clearTestUrls()
+        server.stop()
+    }
+
+    private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+        val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+        assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+                "$urlKey must not be set in production scenarios (current value: $url)")
+    }
+
+    private fun clearTestUrls() {
+        setHttpsUrl(null)
+        setHttpUrl(null)
+        setUrlExpiration(null)
+    }
+
+    @Test
+    fun testCaptivePortalIsNotDefaultNetwork() {
+        assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
+        assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+        utils.connectToWifi()
+        utils.connectToCell()
+
+        // Have network validation use a local server that serves a HTTPS error / HTTP redirect
+        server.addResponse(TEST_PORTAL_URL_PATH, Status.OK,
+                content = "Test captive portal content")
+        server.addResponse(TEST_HTTPS_URL_PATH, Status.INTERNAL_ERROR)
+        server.addResponse(TEST_HTTP_URL_PATH, Status.REDIRECT,
+                locationHeader = server.makeUrl(TEST_PORTAL_URL_PATH))
+        setHttpsUrl(server.makeUrl(TEST_HTTPS_URL_PATH))
+        setHttpUrl(server.makeUrl(TEST_HTTP_URL_PATH))
+        // URL expiration needs to be in the next 10 minutes
+        setUrlExpiration(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
+
+        // Expect the portal content to be fetched at some point after detecting the portal.
+        // Some implementations may fetch the URL before startCaptivePortalApp is called.
+        val portalContentRequestCv = server.addExpectRequestCv(TEST_PORTAL_URL_PATH)
+
+        // Wait for a captive portal to be detected on the network
+        val wifiNetworkFuture = CompletableFuture<Network>()
+        val wifiCb = object : NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                nc: NetworkCapabilities
+            ) {
+                if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+                    wifiNetworkFuture.complete(network)
+                }
+            }
+        }
+        cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
+
+        try {
+            reconnectWifi()
+            val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
+                    "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
+
+            val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
+                    "portal was detected and another network (mobile data) can provide internet " +
+                    "access."
+            assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+
+            doAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
+            assertTrue(portalContentRequestCv.block(TEST_TIMEOUT_MS), "The captive portal login " +
+                    "page was still not fetched ${TEST_TIMEOUT_MS}ms after startCaptivePortalApp.")
+
+            assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
+        } finally {
+            cm.unregisterNetworkCallback(wifiCb)
+            server.stop()
+            // disconnectFromCell should be called after connectToCell
+            utils.disconnectFromCell()
+        }
+
+        clearTestUrls()
+        reconnectWifi()
+    }
+
+    private fun setHttpsUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING, url)
+    private fun setHttpUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING, url)
+    private fun setUrlExpiration(timestamp: Long?) = setConfig(TEST_URL_EXPIRATION_TIME,
+            timestamp?.toString())
+
+    private fun setConfig(configKey: String, value: String?) {
+        doAsShell(WRITE_DEVICE_CONFIG) {
+            DeviceConfig.setProperty(
+                    NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
+        }
+    }
+
+    private fun doAsShell(vararg permissions: String, action: () -> Unit) {
+        // Wrap the below call to allow for more kotlin-like syntax
+        SystemUtil.runWithShellPermissionIdentity(action, permissions)
+    }
+
+    private fun reconnectWifi() {
+        doAsShell(NETWORK_SETTINGS) {
+            assertTrue(wm.disconnect())
+            assertTrue(wm.reconnect())
+        }
+    }
+
+    /**
+     * A minimal HTTP server running on localhost (loopback), on a random available port.
+     */
+    private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
+        // Map of URL path -> HTTP response code
+        private val responses = HashMap<String, Response>()
+
+        // Map of path -> CV to open as soon as a request to the path is received
+        private val waitForRequestCv = HashMap<String, ConditionVariable>()
+
+        /**
+         * Create a URL string that, when fetched, will hit this server with the given URL [path].
+         */
+        fun makeUrl(path: String): String {
+            return Uri.Builder()
+                    .scheme("http")
+                    .encodedAuthority("localhost:$listeningPort")
+                    .query(path)
+                    .build()
+                    .toString()
+        }
+
+        fun addResponse(
+            path: String,
+            statusCode: IStatus,
+            locationHeader: String? = null,
+            content: String = ""
+        ) {
+            val response = newFixedLengthResponse(statusCode, "text/plain", content)
+            locationHeader?.let { response.addHeader("Location", it) }
+            responses[path] = response
+        }
+
+        /**
+         * Create a [ConditionVariable] that will open when a request to [path] is received.
+         */
+        fun addExpectRequestCv(path: String): ConditionVariable {
+            return ConditionVariable().apply { waitForRequestCv[path] = this }
+        }
+
+        override fun serve(session: IHTTPSession): Response {
+            waitForRequestCv[session.queryParameterString]?.open()
+            return responses[session.queryParameterString]
+                    // Default response is a 404
+                    ?: super.serve(session)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
new file mode 100644
index 0000000..1f3162f
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 android.net.cts;
+
+import static android.os.Process.INVALID_UID;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.INetworkStatsService;
+import android.net.TrafficStats;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.AndroidTestCase;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.util.CollectionUtils;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class NetworkStatsBinderTest extends AndroidTestCase {
+    // NOTE: These are shamelessly copied from TrafficStats.
+    private static final int TYPE_RX_BYTES = 0;
+    private static final int TYPE_RX_PACKETS = 1;
+    private static final int TYPE_TX_BYTES = 2;
+    private static final int TYPE_TX_PACKETS = 3;
+
+    private final SparseArray<Function<Integer, Long>> mUidStatsQueryOpArray = new SparseArray<>();
+
+    @Override
+    protected void setUp() throws Exception {
+        mUidStatsQueryOpArray.put(TYPE_RX_BYTES, uid -> TrafficStats.getUidRxBytes(uid));
+        mUidStatsQueryOpArray.put(TYPE_RX_PACKETS, uid -> TrafficStats.getUidRxPackets(uid));
+        mUidStatsQueryOpArray.put(TYPE_TX_BYTES, uid -> TrafficStats.getUidTxBytes(uid));
+        mUidStatsQueryOpArray.put(TYPE_TX_PACKETS, uid -> TrafficStats.getUidTxPackets(uid));
+    }
+
+    private long getUidStatsFromBinder(int uid, int type) throws Exception {
+        Method getServiceMethod = Class.forName("android.os.ServiceManager")
+                .getDeclaredMethod("getService", new Class[]{String.class});
+        IBinder binder = (IBinder) getServiceMethod.invoke(null, Context.NETWORK_STATS_SERVICE);
+        INetworkStatsService nss = INetworkStatsService.Stub.asInterface(binder);
+        return nss.getUidStats(uid, type);
+    }
+
+    private int getFirstAppUidThat(@NonNull Predicate<Integer> predicate) {
+        PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
+        List<PackageInfo> apps = pm.getInstalledPackages(0 /* flags */);
+        final PackageInfo match = CollectionUtils.find(apps,
+                it -> it.applicationInfo != null && predicate.test(it.applicationInfo.uid));
+        if (match != null) return match.applicationInfo.uid;
+        return INVALID_UID;
+    }
+
+    public void testAccessUidStatsFromBinder() throws Exception {
+        final int myUid = Process.myUid();
+        final List<Integer> testUidList = new ArrayList<>();
+
+        // Prepare uid list for testing.
+        testUidList.add(INVALID_UID);
+        testUidList.add(Process.ROOT_UID);
+        testUidList.add(Process.SYSTEM_UID);
+        testUidList.add(myUid);
+        testUidList.add(Process.LAST_APPLICATION_UID);
+        testUidList.add(Process.LAST_APPLICATION_UID + 1);
+        // If available, pick another existing uid for testing that is not already contained
+        // in the list above.
+        final int notMyUid = getFirstAppUidThat(uid -> uid >= 0 && !testUidList.contains(uid));
+        if (notMyUid != INVALID_UID) testUidList.add(notMyUid);
+
+        for (final int uid : testUidList) {
+            for (int i = 0; i < mUidStatsQueryOpArray.size(); i++) {
+                final int type = mUidStatsQueryOpArray.keyAt(i);
+                try {
+                    final long uidStatsFromBinder = getUidStatsFromBinder(uid, type);
+                    final long uidTrafficStats = mUidStatsQueryOpArray.get(type).apply(uid);
+
+                    // Verify that UNSUPPORTED is returned if the uid is not current app uid.
+                    if (uid != myUid) {
+                        assertEquals(uidStatsFromBinder, TrafficStats.UNSUPPORTED);
+                    }
+                    // Verify that returned result is the same with the result get from
+                    // TrafficStats.
+                    // TODO: If the test is flaky then it should instead assert that the values
+                    //  are approximately similar.
+                    assertEquals("uidStats is not matched for query type " + type
+                                    + ", uid=" + uid + ", myUid=" + myUid, uidTrafficStats,
+                            uidStatsFromBinder);
+                } catch (IllegalAccessException e) {
+                    /* Java language access prevents exploitation. */
+                    return;
+                } catch (InvocationTargetException e) {
+                    /* Underlying method has been changed. */
+                    return;
+                } catch (ClassNotFoundException e) {
+                    /* not vulnerable if hidden API no longer available */
+                    return;
+                } catch (NoSuchMethodException e) {
+                    /* not vulnerable if hidden API no longer available */
+                    return;
+                } catch (RemoteException e) {
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 6214f89..824146f 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -16,6 +16,7 @@
 
 package android.net.cts.util;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
@@ -107,6 +108,8 @@
         boolean connected = false;
         try {
             SystemUtil.runShellCommand("svc wifi enable");
+            SystemUtil.runWithShellPermissionIdentity(() -> mWifiManager.reconnect(),
+                    NETWORK_SETTINGS);
             // Ensure we get both an onAvailable callback and a CONNECTIVITY_ACTION.
             wifiNetwork = callback.waitForAvailable();
             assertNotNull(wifiNetwork);