Merge "Include the failing stats in stats failure assertions."
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index e27c811..8e29ed3 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -26,6 +26,7 @@
         "libtetherutilsjni",
     ],
     jni_uses_sdk_apis: true,
+    jarjar_rules: ":TetheringTestsJarJarRules",
     visibility: ["//visibility:private"],
 }
 
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index e7a40e5..1a0de32 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -66,6 +66,8 @@
     // Activate interface using an unconnected datagram socket.
     base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
     ifr.ifr_flags = IFF_UP;
+    // Mark TAP interfaces as supporting multicast
+    if (!isTun) ifr.ifr_flags |= IFF_MULTICAST;
 
     if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
         throwException(env, errno, "activating", ifr.ifr_name);
diff --git a/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
index c480594..acfbb3c 100644
--- a/service/src/com/android/server/connectivity/TcpKeepaliveController.java
+++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
@@ -16,6 +16,7 @@
 package com.android.server.connectivity;
 
 import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
 import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
 import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
 import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
@@ -29,6 +30,8 @@
 import static android.system.OsConstants.IP_TTL;
 import static android.system.OsConstants.TIOCOUTQ;
 
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+
 import android.annotation.NonNull;
 import android.net.InvalidPacketException;
 import android.net.NetworkUtils;
@@ -36,7 +39,6 @@
 import android.net.TcpKeepalivePacketData;
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.TcpRepairWindow;
-import android.net.util.KeepalivePacketDataUtil;
 import android.os.Handler;
 import android.os.MessageQueue;
 import android.os.Messenger;
@@ -46,12 +48,18 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.IpUtils;
 import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
 
 import java.io.FileDescriptor;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * Manage tcp socket which offloads tcp keepalive.
@@ -82,6 +90,8 @@
 
     private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
 
+    private static final int TCP_HEADER_LENGTH = 20;
+
     // Reference include/uapi/linux/tcp.h
     private static final int TCP_REPAIR = 19;
     private static final int TCP_REPAIR_QUEUE = 20;
@@ -112,12 +122,81 @@
             throws InvalidPacketException, InvalidSocketException {
         try {
             final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
-            return KeepalivePacketDataUtil.fromStableParcelable(tcpDetails);
+            // TODO: consider building a TcpKeepalivePacketData directly from switchToRepairMode
+            return fromStableParcelable(tcpDetails);
         } catch (InvalidPacketException | InvalidSocketException e) {
             switchOutOfRepairMode(fd);
             throw e;
         }
     }
+
+    /**
+     * Factory method to create tcp keepalive packet structure.
+     */
+    @VisibleForTesting
+    public static TcpKeepalivePacketData fromStableParcelable(
+            TcpKeepalivePacketDataParcelable tcpDetails) throws InvalidPacketException {
+        final byte[] packet;
+        try {
+            if ((tcpDetails.srcAddress != null) && (tcpDetails.dstAddress != null)
+                    && (tcpDetails.srcAddress.length == 4 /* V4 IP length */)
+                    && (tcpDetails.dstAddress.length == 4 /* V4 IP length */)) {
+                packet = buildV4Packet(tcpDetails);
+            } else {
+                // TODO: support ipv6
+                throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+            }
+            return new TcpKeepalivePacketData(
+                    InetAddress.getByAddress(tcpDetails.srcAddress),
+                    tcpDetails.srcPort,
+                    InetAddress.getByAddress(tcpDetails.dstAddress),
+                    tcpDetails.dstPort,
+                    packet,
+                    tcpDetails.seq, tcpDetails.ack, tcpDetails.rcvWnd, tcpDetails.rcvWndScale,
+                    tcpDetails.tos, tcpDetails.ttl);
+        } catch (UnknownHostException e) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+    }
+
+    /**
+     * Build ipv4 tcp keepalive packet, not including the link-layer header.
+     */
+    // TODO : if this code is ever moved to the network stack, factorize constants with the ones
+    // over there.
+    // TODO: consider using Ipv4Utils.buildTcpv4Packet() instead
+    private static byte[] buildV4Packet(TcpKeepalivePacketDataParcelable tcpDetails) {
+        final int length = IPV4_HEADER_MIN_LEN + TCP_HEADER_LENGTH;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.put((byte) 0x45);                       // IP version and IHL
+        buf.put((byte) tcpDetails.tos);             // TOS
+        buf.putShort((short) length);
+        buf.putInt(0x00004000);                     // ID, flags=DF, offset
+        buf.put((byte) tcpDetails.ttl);             // TTL
+        buf.put((byte) IPPROTO_TCP);
+        final int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // IP checksum
+        buf.put(tcpDetails.srcAddress);
+        buf.put(tcpDetails.dstAddress);
+        buf.putShort((short) tcpDetails.srcPort);
+        buf.putShort((short) tcpDetails.dstPort);
+        buf.putInt(tcpDetails.seq);                 // Sequence Number
+        buf.putInt(tcpDetails.ack);                 // ACK
+        buf.putShort((short) 0x5010);               // TCP length=5, flags=ACK
+        buf.putShort((short) (tcpDetails.rcvWnd >> tcpDetails.rcvWndScale));   // Window size
+        final int tcpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // TCP checksum
+        // URG is not set therefore the urgent pointer is zero.
+        buf.putShort((short) 0);                    // Urgent pointer
+
+        buf.putShort(ipChecksumOffset, com.android.net.module.util.IpUtils.ipChecksum(buf, 0));
+        buf.putShort(tcpChecksumOffset, IpUtils.tcpChecksum(
+                buf, 0, IPV4_HEADER_MIN_LEN, TCP_HEADER_LENGTH));
+
+        return buf.array();
+    }
+
     /**
      * Switch the tcp socket to repair mode and query detail tcp information.
      *
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 85942b0..1872327 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -60,6 +60,7 @@
     // uncomment when b/13249961 is fixed
     // sdk_version: "current",
     platform_apis: true,
+    required: ["ConnectivityChecker"],
 }
 
 // Networking CTS tests for development and release. These tests always target the platform SDK
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 78a01e2..d761c27 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -26,6 +26,8 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.cts" />
         <option name="runtime-hint" value="9m4s" />
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 7d9a24b..e17200e 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,15 +16,11 @@
 
 package android.net.cts.util;
 
-import static android.Manifest.permission.ACCESS_WIFI_STATE;
-import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
-import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
 
 import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -48,15 +44,12 @@
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
 import android.net.TestNetworkManager;
-import android.net.wifi.ScanResult;
-import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.ConditionVariable;
 import android.os.IBinder;
-import android.os.SystemClock;
 import android.system.Os;
 import android.system.OsConstants;
 import android.text.TextUtils;
@@ -64,23 +57,17 @@
 
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.net.module.util.ConnectivitySettingsUtils;
-
-import junit.framework.AssertionFailedError;
+import com.android.testutils.ConnectUtil;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.InetSocketAddress;
 import java.net.Socket;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
 
 public final class CtsNetUtils {
     private static final String TAG = CtsNetUtils.class.getSimpleName();
@@ -89,13 +76,7 @@
 
     private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
     private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
-    private static final int MAX_WIFI_CONNECT_RETRIES = 10;
-    private static final int WIFI_CONNECT_INTERVAL_MS = 500;
 
-    // Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
-    // the error code constants are not (they probably should be ?)
-    private static final int WIFI_ERROR_IN_PROGRESS = 1;
-    private static final int WIFI_ERROR_BUSY = 2;
     private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
     private static final String PRIVATE_DNS_MODE_STRICT = "hostname";
     public static final int HTTP_PORT = 80;
@@ -237,10 +218,6 @@
      * @return The network that was newly connected.
      */
     private Network connectToWifi(boolean expectLegacyBroadcast) {
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
-        Network wifiNetwork = null;
-
         ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
                 mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
         IntentFilter filter = new IntentFilter();
@@ -248,159 +225,17 @@
         mContext.registerReceiver(receiver, filter);
 
         try {
-            // Clear the wifi config blocklist (not the BSSID blocklist)
-            clearWifiBlocklist();
-            SystemUtil.runShellCommand("svc wifi enable");
-            final WifiConfiguration config = getOrCreateWifiConfiguration();
-            connectToWifiConfig(config);
-
-            // Ensure we get an onAvailable callback and possibly a CONNECTIVITY_ACTION.
-            wifiNetwork = callback.waitForAvailable();
-            assertNotNull("onAvailable callback not received after connecting to " + config.SSID,
-                    wifiNetwork);
+            final Network network = new ConnectUtil(mContext).ensureWifiConnected();
             if (expectLegacyBroadcast) {
-                assertTrue("CONNECTIVITY_ACTION not received after connecting to " + config.SSID,
+                assertTrue("CONNECTIVITY_ACTION not received after connecting to " + network,
                         receiver.waitForState());
             }
+            return network;
         } catch (InterruptedException ex) {
-            fail("connectToWifi was interrupted");
+            throw new AssertionError("connectToWifi was interrupted", ex);
         } finally {
-            mCm.unregisterNetworkCallback(callback);
             mContext.unregisterReceiver(receiver);
         }
-
-        return wifiNetwork;
-    }
-
-    private void connectToWifiConfig(WifiConfiguration config) {
-        for (int i = 0; i < MAX_WIFI_CONNECT_RETRIES; i++) {
-            final Integer error = runAsShell(NETWORK_SETTINGS, () -> {
-                final ConnectWifiListener listener = new ConnectWifiListener();
-                mWifiManager.connect(config, listener);
-                return listener.connectFuture.get(
-                        CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
-            });
-
-            if (error == null) return;
-
-            // Only retry for IN_PROGRESS and BUSY
-            if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
-                fail("Failed to connect to " + config.SSID + ": " + error);
-            }
-
-            Log.w(TAG, "connect failed with " + error + "; waiting before retry");
-            SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS);
-        }
-
-        fail("Failed to connect to " + config.SSID
-                + " after " + MAX_WIFI_CONNECT_RETRIES + "retries");
-    }
-
-    private static class ConnectWifiListener implements WifiManager.ActionListener {
-        /**
-         * Future completed when the connect process ends. Provides the error code or null if none.
-         */
-        final CompletableFuture<Integer> connectFuture = new CompletableFuture<>();
-        @Override
-        public void onSuccess() {
-            connectFuture.complete(null);
-        }
-
-        @Override
-        public void onFailure(int reason) {
-            connectFuture.complete(reason);
-        }
-    }
-
-    private WifiConfiguration getOrCreateWifiConfiguration() {
-        final List<WifiConfiguration> configs = runAsShell(NETWORK_SETTINGS,
-                mWifiManager::getConfiguredNetworks);
-        // If no network is configured, add a config for virtual access points if applicable
-        if (configs.size() == 0) {
-            final List<ScanResult> scanResults = getWifiScanResults();
-            final WifiConfiguration virtualConfig = maybeConfigureVirtualNetwork(scanResults);
-            assertNotNull("The device has no configured wifi network", virtualConfig);
-
-            return virtualConfig;
-        }
-        // No need to add a configuration: there is already one.
-        if (configs.size() > 1) {
-            // For convenience in case of local testing on devices with multiple saved configs,
-            // prefer the first configuration that is in range.
-            // In actual tests, there should only be one configuration, and it should be usable as
-            // assumed by WifiManagerTest.testConnect.
-            Log.w(TAG, "Multiple wifi configurations found: "
-                    + configs.stream().map(c -> c.SSID).collect(Collectors.joining(", ")));
-            final List<ScanResult> scanResultsList = getWifiScanResults();
-            Log.i(TAG, "Scan results: " + scanResultsList.stream().map(c ->
-                    c.SSID + " (" + c.level + ")").collect(Collectors.joining(", ")));
-            final Set<String> scanResults = scanResultsList.stream().map(
-                    s -> "\"" + s.SSID + "\"").collect(Collectors.toSet());
-
-            return configs.stream().filter(c -> scanResults.contains(c.SSID))
-                    .findFirst().orElse(configs.get(0));
-        }
-        return configs.get(0);
-    }
-
-    private List<ScanResult> getWifiScanResults() {
-        final CompletableFuture<List<ScanResult>> scanResultsFuture = new CompletableFuture<>();
-        runAsShell(NETWORK_SETTINGS, () -> {
-            final BroadcastReceiver receiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    scanResultsFuture.complete(mWifiManager.getScanResults());
-                }
-            };
-            mContext.registerReceiver(receiver, new IntentFilter(SCAN_RESULTS_AVAILABLE_ACTION));
-            mWifiManager.startScan();
-        });
-
-        try {
-            return scanResultsFuture.get(CONNECTIVITY_CHANGE_TIMEOUT_SECS, TimeUnit.SECONDS);
-        } catch (ExecutionException | InterruptedException | TimeoutException e) {
-            throw new AssertionFailedError("Wifi scan results not received within timeout");
-        }
-    }
-
-    /**
-     * If a virtual wifi network is detected, add a configuration for that network.
-     * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
-     */
-    private WifiConfiguration maybeConfigureVirtualNetwork(List<ScanResult> scanResults) {
-        // Virtual wifi networks used on the emulator and cloud testing infrastructure
-        final List<String> virtualSsids = Arrays.asList("VirtWifi", "AndroidWifi");
-        Log.d(TAG, "Wifi scan results: " + scanResults);
-        final ScanResult virtualScanResult = scanResults.stream().filter(
-                s -> virtualSsids.contains(s.SSID)).findFirst().orElse(null);
-
-        // Only add the virtual configuration if the virtual AP is detected in scans
-        if (virtualScanResult == null) return null;
-
-        final WifiConfiguration virtualConfig = new WifiConfiguration();
-        // ASCII SSIDs need to be surrounded by double quotes
-        virtualConfig.SSID = "\"" + virtualScanResult.SSID + "\"";
-        virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
-
-        runAsShell(NETWORK_SETTINGS, () -> {
-            final int networkId = mWifiManager.addNetwork(virtualConfig);
-            assertTrue(networkId >= 0);
-            assertTrue(mWifiManager.enableNetwork(networkId, false /* attemptConnect */));
-        });
-        return virtualConfig;
-    }
-
-    /**
-     * Re-enable wifi networks that were blocklisted, typically because no internet connection was
-     * detected the last time they were connected. This is necessary to make sure wifi can reconnect
-     * to them.
-     */
-    private void clearWifiBlocklist() {
-        runAsShell(NETWORK_SETTINGS, ACCESS_WIFI_STATE, () -> {
-            for (WifiConfiguration cfg : mWifiManager.getConfiguredNetworks()) {
-                assertTrue(mWifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */));
-            }
-        });
     }
 
     /**
diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
index 8498b6f..6afa4e9 100644
--- a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
+++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
@@ -27,6 +27,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import com.android.server.connectivity.TcpKeepaliveController;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -81,7 +82,7 @@
         testInfo.tos = tos;
         testInfo.ttl = ttl;
         try {
-            resultData = KeepalivePacketDataUtil.fromStableParcelable(testInfo);
+            resultData = TcpKeepaliveController.fromStableParcelable(testInfo);
         } catch (InvalidPacketException e) {
             fail("InvalidPacketException: " + e);
         }
@@ -155,7 +156,7 @@
         testInfo.ttl = ttl;
         TcpKeepalivePacketData testData = null;
         TcpKeepalivePacketDataParcelable resultData = null;
-        testData = KeepalivePacketDataUtil.fromStableParcelable(testInfo);
+        testData = TcpKeepaliveController.fromStableParcelable(testInfo);
         resultData = KeepalivePacketDataUtil.toStableParcelable(testData);
         assertArrayEquals(resultData.srcAddress, IPV4_KEEPALIVE_SRC_ADDR);
         assertArrayEquals(resultData.dstAddress, IPV4_KEEPALIVE_DST_ADDR);
@@ -198,11 +199,11 @@
         testParcel.ttl = ttl;
 
         final KeepalivePacketData testData =
-                KeepalivePacketDataUtil.fromStableParcelable(testParcel);
+                TcpKeepaliveController.fromStableParcelable(testParcel);
         final TcpKeepalivePacketDataParcelable parsedParcelable =
                 KeepalivePacketDataUtil.parseTcpKeepalivePacketData(testData);
         final TcpKeepalivePacketData roundTripData =
-                KeepalivePacketDataUtil.fromStableParcelable(parsedParcelable);
+                TcpKeepaliveController.fromStableParcelable(parsedParcelable);
 
         // Generated packet is the same, but rcvWnd / wndScale will differ if scale is non-zero
         assertTrue(testData.getPacket().length > 0);
@@ -210,11 +211,11 @@
 
         testParcel.rcvWndScale = 0;
         final KeepalivePacketData noScaleTestData =
-                KeepalivePacketDataUtil.fromStableParcelable(testParcel);
+                TcpKeepaliveController.fromStableParcelable(testParcel);
         final TcpKeepalivePacketDataParcelable noScaleParsedParcelable =
                 KeepalivePacketDataUtil.parseTcpKeepalivePacketData(noScaleTestData);
         final TcpKeepalivePacketData noScaleRoundTripData =
-                KeepalivePacketDataUtil.fromStableParcelable(noScaleParsedParcelable);
+                TcpKeepaliveController.fromStableParcelable(noScaleParsedParcelable);
         assertEquals(noScaleTestData, noScaleRoundTripData);
         assertTrue(noScaleTestData.getPacket().length > 0);
         assertArrayEquals(noScaleTestData.getPacket(), noScaleRoundTripData.getPacket());
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 2db77f2..572c1ef 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -46,6 +46,7 @@
 import android.net.NetworkTemplate.buildTemplateMobileWithRatType
 import android.net.NetworkTemplate.buildTemplateWifi
 import android.net.NetworkTemplate.buildTemplateWifiWildcard
+import android.net.NetworkTemplate.normalize
 import android.os.Build
 import android.telephony.TelephonyManager
 import com.android.testutils.DevSdkIgnoreRule
@@ -63,6 +64,7 @@
 
 private const val TEST_IMSI1 = "imsi1"
 private const val TEST_IMSI2 = "imsi2"
+private const val TEST_IMSI3 = "imsi3"
 private const val TEST_SSID1 = "ssid1"
 private const val TEST_SSID2 = "ssid2"
 
@@ -493,4 +495,45 @@
                 identSsid = TEST_SSID1)
         matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI_WILDCARD, identSsid = TEST_SSID1)
     }
+
+    @Test
+    fun testNormalize() {
+        var mergedImsiList = listOf(arrayOf(TEST_IMSI1, TEST_IMSI2))
+        val identMobileImsi1 = buildNetworkIdentity(mockContext,
+                buildMobileNetworkState(TEST_IMSI1), false /* defaultNetwork */,
+                TelephonyManager.NETWORK_TYPE_UMTS)
+        val identMobileImsi2 = buildNetworkIdentity(mockContext,
+                buildMobileNetworkState(TEST_IMSI2), false /* defaultNetwork */,
+                TelephonyManager.NETWORK_TYPE_UMTS)
+        val identMobileImsi3 = buildNetworkIdentity(mockContext,
+                buildMobileNetworkState(TEST_IMSI3), false /* defaultNetwork */,
+                TelephonyManager.NETWORK_TYPE_UMTS)
+        val identWifiImsi1Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0)
+        val identWifiImsi2Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0)
+        val identWifiImsi3Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI3, TEST_SSID1), true, 0)
+
+        normalize(buildTemplateMobileAll(TEST_IMSI1), mergedImsiList).also {
+            it.assertMatches(identMobileImsi1)
+            it.assertMatches(identMobileImsi2)
+            it.assertDoesNotMatch(identMobileImsi3)
+        }
+        normalize(buildTemplateCarrierMetered(TEST_IMSI1), mergedImsiList).also {
+            it.assertMatches(identMobileImsi1)
+            it.assertMatches(identMobileImsi2)
+            it.assertDoesNotMatch(identMobileImsi3)
+        }
+        normalize(buildTemplateWifi(TEST_SSID1, TEST_IMSI1), mergedImsiList).also {
+            it.assertMatches(identWifiImsi1Ssid1)
+            it.assertMatches(identWifiImsi2Ssid1)
+            it.assertDoesNotMatch(identWifiImsi3Ssid1)
+        }
+        normalize(buildTemplateMobileWildcard(), mergedImsiList).also {
+            it.assertMatches(identMobileImsi1)
+            it.assertMatches(identMobileImsi2)
+            it.assertMatches(identMobileImsi3)
+        }
+    }
 }