Merge "Add NetworkStack utilities for reading text"
diff --git a/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java b/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
index 29d8506..762a8b8 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
+++ b/apishim/29/com/android/networkstack/apishim/api29/ConstantsShim.java
@@ -24,7 +24,7 @@
     public static final int DETECTION_METHOD_DNS_EVENTS = 1;
     public static final int DETECTION_METHOD_TCP_METRICS = 2;
     public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
-    public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttemped";
+    public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted";
     public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded";
     public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
     public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS =
diff --git a/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java b/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java
index 00b4e19..7de376a 100644
--- a/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java
+++ b/common/moduleutils/src/android/net/shared/ProvisioningConfiguration.java
@@ -16,14 +16,25 @@
 
 package android.net.shared;
 
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.INetd;
+import android.net.InformationElementParcelable;
 import android.net.Network;
 import android.net.ProvisioningConfigurationParcelable;
+import android.net.ScanResultInfoParcelable;
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
 import android.net.ip.IIpClient;
 
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.StringJoiner;
 
@@ -193,6 +204,17 @@
         }
 
         /**
+         * Specify the information elements included in wifi scan result that was obtained
+         * prior to connecting to the access point, if this is a WiFi network.
+         *
+         * <p>The scan result can be used to infer whether the network is metered.
+         */
+        public Builder withScanResultInfo(ScanResultInfo scanResultInfo) {
+            mConfig.mScanResultInfo = scanResultInfo;
+            return this;
+        }
+
+        /**
          * Build the configuration using previously specified parameters.
          */
         public ProvisioningConfiguration build() {
@@ -200,6 +222,158 @@
         }
     }
 
+    /**
+     * Class wrapper of {@link android.net.wifi.ScanResult} to encapsulate the SSID and
+     * InformationElements fields of ScanResult.
+     */
+    public static class ScanResultInfo {
+        private final String mSsid;
+        private final List<InformationElement> mInformationElements;
+
+       /**
+        * Class wrapper of {@link android.net.wifi.ScanResult.InformationElement} to encapsulate
+        * the specific IE id and payload fields.
+        */
+        public static class InformationElement {
+            private final int mId;
+            private final byte[] mPayload;
+
+            public InformationElement(int id, @NonNull ByteBuffer payload) {
+                mId = id;
+                mPayload = convertToByteArray(payload.asReadOnlyBuffer());
+            }
+
+           /**
+            * Get the element ID of the information element.
+            */
+            public int getId() {
+                return mId;
+            }
+
+           /**
+            * Get the specific content of the information element.
+            */
+            public ByteBuffer getPayload() {
+                return ByteBuffer.wrap(mPayload).asReadOnlyBuffer();
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (o == this) return true;
+                if (!(o instanceof InformationElement)) return false;
+                InformationElement other = (InformationElement) o;
+                return mId == other.mId && Arrays.equals(mPayload, other.mPayload);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mId, mPayload);
+            }
+
+            @Override
+            public String toString() {
+                return "ID: " + mId + ", " + Arrays.toString(mPayload);
+            }
+
+            /**
+             * Convert this InformationElement to a {@link InformationElementParcelable}.
+             */
+            public InformationElementParcelable toStableParcelable() {
+                final InformationElementParcelable p = new InformationElementParcelable();
+                p.id = mId;
+                p.payload = mPayload.clone();
+                return p;
+            }
+
+            /**
+             * Create an instance of {@link InformationElement} based on the contents of the
+             * specified {@link InformationElementParcelable}.
+             */
+            public static InformationElement fromStableParcelable(InformationElementParcelable p) {
+                if (p == null) return null;
+                return new InformationElement(p.id,
+                        ByteBuffer.wrap(p.payload.clone()).asReadOnlyBuffer());
+            }
+        }
+
+        public ScanResultInfo(String ssid, @NonNull List<InformationElement> informationElements) {
+            mSsid = ssid;
+            mInformationElements =
+                    Collections.unmodifiableList(new ArrayList<>(informationElements));
+        }
+
+        /**
+         * Get the scanned network name.
+         */
+        public String getSsid() {
+            return mSsid;
+        }
+
+        /**
+         * Get all information elements found in the beacon.
+         */
+        public List<InformationElement> getInformationElements() {
+            return mInformationElements;
+        }
+
+        @Override
+        public String toString() {
+            StringBuffer str = new StringBuffer();
+            str.append("SSID: ").append(mSsid);
+            str.append(", Information Elements: {");
+            for (InformationElement ie : mInformationElements) {
+                str.append("[").append(ie.toString()).append("]");
+            }
+            str.append("}");
+            return str.toString();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) return true;
+            if (!(o instanceof ScanResultInfo)) return false;
+            ScanResultInfo other = (ScanResultInfo) o;
+            return Objects.equals(mSsid, other.mSsid)
+                    && mInformationElements.equals(other.mInformationElements);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mSsid, mInformationElements);
+        }
+
+        /**
+         * Convert this ScanResultInfo to a {@link ScanResultInfoParcelable}.
+         */
+        public ScanResultInfoParcelable toStableParcelable() {
+            final ScanResultInfoParcelable p = new ScanResultInfoParcelable();
+            p.ssid = mSsid;
+            p.informationElements = toParcelableArray(mInformationElements,
+                    InformationElement::toStableParcelable, InformationElementParcelable.class);
+            return p;
+        }
+
+        /**
+         * Create an instance of {@link ScanResultInfo} based on the contents of the specified
+         * {@link ScanResultInfoParcelable}.
+         */
+        public static ScanResultInfo fromStableParcelable(ScanResultInfoParcelable p) {
+            if (p == null) return null;
+            final List<InformationElement> ies = new ArrayList<InformationElement>();
+            ies.addAll(fromParcelableArray(p.informationElements,
+                    InformationElement::fromStableParcelable));
+            return new ScanResultInfo(p.ssid, ies);
+        }
+
+        private static byte[] convertToByteArray(final ByteBuffer buffer) {
+            if (buffer == null) return null;
+            byte[] bytes = new byte[buffer.limit()];
+            final ByteBuffer copy = buffer.asReadOnlyBuffer();
+            copy.get(bytes);
+            return bytes;
+        }
+    }
+
     public boolean mEnableIPv4 = true;
     public boolean mEnableIPv6 = true;
     public boolean mEnablePreconnection = false;
@@ -213,6 +387,7 @@
     public int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
     public Network mNetwork = null;
     public String mDisplayName = null;
+    public ScanResultInfo mScanResultInfo;
 
     public ProvisioningConfiguration() {} // used by Builder
 
@@ -232,6 +407,7 @@
         mIPv6AddrGenMode = other.mIPv6AddrGenMode;
         mNetwork = other.mNetwork;
         mDisplayName = other.mDisplayName;
+        mScanResultInfo = other.mScanResultInfo;
     }
 
     /**
@@ -254,6 +430,7 @@
         p.ipv6AddrGenMode = mIPv6AddrGenMode;
         p.network = mNetwork;
         p.displayName = mDisplayName;
+        p.scanResultInfo = mScanResultInfo == null ? null : mScanResultInfo.toStableParcelable();
         return p;
     }
 
@@ -279,6 +456,7 @@
         config.mIPv6AddrGenMode = p.ipv6AddrGenMode;
         config.mNetwork = p.network;
         config.mDisplayName = p.displayName;
+        config.mScanResultInfo = ScanResultInfo.fromStableParcelable(p.scanResultInfo);
         return config;
     }
 
@@ -298,6 +476,7 @@
                 .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
                 .add("mNetwork: " + mNetwork)
                 .add("mDisplayName: " + mDisplayName)
+                .add("mScanResultInfo: " + mScanResultInfo)
                 .toString();
     }
 
@@ -317,7 +496,8 @@
                 && mProvisioningTimeoutMs == other.mProvisioningTimeoutMs
                 && mIPv6AddrGenMode == other.mIPv6AddrGenMode
                 && Objects.equals(mNetwork, other.mNetwork)
-                && Objects.equals(mDisplayName, other.mDisplayName);
+                && Objects.equals(mDisplayName, other.mDisplayName)
+                && Objects.equals(mScanResultInfo, other.mScanResultInfo);
     }
 
     public boolean isValid() {
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index 31f3384..dbe8ff0 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -45,6 +45,7 @@
     include_dirs: [
         "frameworks/base/core/java", // For framework parcelables.
         "frameworks/native/aidl/binder/android/os", // For PersistableBundle.aidl
+        "frameworks/base/wifi/aidl-export", // For wifi parcelables.
     ],
     srcs: [
         "src/android/net/DhcpResultsParcelable.aidl",
@@ -53,10 +54,12 @@
         "src/android/net/INetworkStackConnector.aidl",
         "src/android/net/INetworkStackStatusCallback.aidl",
         "src/android/net/InitialConfigurationParcelable.aidl",
+        "src/android/net/InformationElementParcelable.aidl",
         "src/android/net/Layer2PacketParcelable.aidl",
         "src/android/net/NattKeepalivePacketDataParcelable.aidl",
         "src/android/net/PrivateDnsConfigParcel.aidl",
         "src/android/net/ProvisioningConfigurationParcelable.aidl",
+        "src/android/net/ScanResultInfoParcelable.aidl",
         "src/android/net/TcpKeepalivePacketDataParcelable.aidl",
         "src/android/net/dhcp/DhcpServingParamsParcel.aidl",
         "src/android/net/dhcp/IDhcpServer.aidl",
diff --git a/common/networkstackclient/src/android/net/InformationElementParcelable.aidl b/common/networkstackclient/src/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..c70bf6f
--- /dev/null
+++ b/common/networkstackclient/src/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+parcelable InformationElementParcelable {
+    int id;
+    byte[] payload;
+}
diff --git a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
index 0b6d7d5..9fcb036 100644
--- a/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
+++ b/common/networkstackclient/src/android/net/ProvisioningConfigurationParcelable.aidl
@@ -19,6 +19,7 @@
 
 import android.net.InitialConfigurationParcelable;
 import android.net.Network;
+import android.net.ScanResultInfoParcelable;
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
 
@@ -36,4 +37,5 @@
     Network network;
     String displayName;
     boolean enablePreconnection;
+    ScanResultInfoParcelable scanResultInfo;
 }
diff --git a/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..f5f101d
--- /dev/null
+++ b/common/networkstackclient/src/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+import android.net.InformationElementParcelable;
+
+parcelable ScanResultInfoParcelable {
+    String ssid;
+    InformationElementParcelable[] informationElements;
+}
diff --git a/src/com/android/networkstack/netlink/TcpInfo.java b/src/com/android/networkstack/netlink/TcpInfo.java
index e6036b5..31a408f 100644
--- a/src/com/android/networkstack/netlink/TcpInfo.java
+++ b/src/com/android/networkstack/netlink/TcpInfo.java
@@ -22,11 +22,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.nio.BufferOverflowException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -91,27 +89,39 @@
     }
 
     private static final String TAG = "TcpInfo";
-    private final Map<Field, Number> mFieldsValues;
+    @VisibleForTesting
+    static final int LOST_OFFSET = getFieldOffset(Field.LOST);
+    @VisibleForTesting
+    static final int RETRANSMITS_OFFSET = getFieldOffset(Field.RETRANSMITS);
+    @VisibleForTesting
+    static final int SEGS_IN_OFFSET = getFieldOffset(Field.SEGS_IN);
+    @VisibleForTesting
+    static final int SEGS_OUT_OFFSET = getFieldOffset(Field.SEGS_OUT);
+    final int mSegsIn;
+    final int mSegsOut;
+    final int mLost;
+    final int mRetransmits;
+
+    private static int getFieldOffset(@NonNull final Field needle) {
+        int offset = 0;
+        for (final Field field : Field.values()) {
+            if (field == needle) return offset;
+            offset += field.size;
+        }
+        throw new IllegalArgumentException("Unknown field");
+    }
 
     private TcpInfo(@NonNull ByteBuffer bytes, int infolen) {
-        final int start = bytes.position();
-        final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
-        for (final Field field : Field.values()) {
-            switch (field.size) {
-                case Byte.BYTES:
-                    fields.put(field, getByte(bytes, start, infolen));
-                    break;
-                case Integer.BYTES:
-                    fields.put(field, getInt(bytes, start, infolen));
-                    break;
-                case Long.BYTES:
-                    fields.put(field, getLong(bytes, start, infolen));
-                    break;
-                default:
-                    Log.e(TAG, "Unexpected size:" + field.size);
-            }
+        // SEGS_IN is the last required field in the buffer, so if the buffer is long enough for
+        // SEGS_IN it's long enough for everything
+        if (SEGS_IN_OFFSET + Field.SEGS_IN.size > infolen) {
+            throw new IllegalArgumentException("Length " + infolen + " is less than required.");
         }
-        mFieldsValues = Collections.unmodifiableMap(fields);
+        final int start = bytes.position();
+        mSegsIn = bytes.getInt(start + SEGS_IN_OFFSET);
+        mSegsOut = bytes.getInt(start + SEGS_OUT_OFFSET);
+        mLost = bytes.getInt(start + LOST_OFFSET);
+        mRetransmits = bytes.get(start + RETRANSMITS_OFFSET);
         // tcp_info structure grows over time as new fields are added. Jump to the end of the
         // structure, as unknown fields might remain at the end of the structure if the tcp_info
         // struct was expanded.
@@ -119,12 +129,11 @@
     }
 
     @VisibleForTesting
-    TcpInfo(@NonNull Map<Field, Number> info) {
-        final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
-        for (final Field field : Field.values()) {
-            fields.put(field, info.get(field));
-        }
-        mFieldsValues = Collections.unmodifiableMap(fields);
+    TcpInfo(int retransmits, int lost, int segsOut, int segsIn) {
+        mRetransmits = retransmits;
+        mLost = lost;
+        mSegsOut = segsOut;
+        mSegsIn = segsIn;
     }
 
     /** Parse a TcpInfo from a giving ByteBuffer with a specific length. */
@@ -132,53 +141,13 @@
     public static TcpInfo parse(@NonNull ByteBuffer bytes, int infolen) {
         try {
             return new TcpInfo(bytes, infolen);
-        } catch (BufferUnderflowException | IllegalArgumentException e) {
+        } catch (BufferUnderflowException | BufferOverflowException | IllegalArgumentException
+                | IndexOutOfBoundsException e) {
             Log.e(TAG, "parsing error.", e);
             return null;
         }
     }
 
-    /**
-     * Helper function for handling different struct tcp_info versions in the kernel.
-     */
-    private static boolean isValidTargetPosition(int start, int len, int pos, int targetBytes)
-            throws IllegalArgumentException {
-        // Equivalent to new Range(start, start + len).contains(new Range(pos, pos + targetBytes))
-        if (len < 0 || targetBytes < 0) throw new IllegalArgumentException();
-        // Check that start < pos < start + len
-        if (pos < start || pos > start + len) return false;
-        // Pos is inside the range and targetBytes is positive. Offset is valid if end of 2nd range
-        // is below end of 1st range.
-        return pos + targetBytes <= start + len;
-    }
-
-    /** Get value for specific key. */
-    @Nullable
-    public Number getValue(@NonNull Field key) {
-        return mFieldsValues.get(key);
-    }
-
-    @Nullable
-    private static Byte getByte(@NonNull ByteBuffer buffer, int start, int len) {
-        if (!isValidTargetPosition(start, len, buffer.position(), Byte.BYTES)) return null;
-
-        return buffer.get();
-    }
-
-    @Nullable
-    private static Integer getInt(@NonNull ByteBuffer buffer, int start, int len) {
-        if (!isValidTargetPosition(start, len, buffer.position(), Integer.BYTES)) return null;
-
-        return buffer.getInt();
-    }
-
-    @Nullable
-    private static Long getLong(@NonNull ByteBuffer buffer, int start, int len) {
-        if (!isValidTargetPosition(start, len, buffer.position(), Long.BYTES)) return null;
-
-        return buffer.getLong();
-    }
-
     private static String decodeWscale(byte num) {
         return String.valueOf((num >> 4) & 0x0f)  + ":" + String.valueOf(num & 0x0f);
     }
@@ -210,33 +179,18 @@
         if (!(obj instanceof TcpInfo)) return false;
         TcpInfo other = (TcpInfo) obj;
 
-        for (final Field key : mFieldsValues.keySet()) {
-            if (!Objects.equals(mFieldsValues.get(key), other.mFieldsValues.get(key))) {
-                return false;
-            }
-        }
-        return true;
+        return mSegsIn == other.mSegsIn && mSegsOut == other.mSegsOut
+            && mRetransmits == other.mRetransmits && mLost == other.mLost;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mFieldsValues.values().toArray());
+        return Objects.hash(mLost, mRetransmits, mSegsIn, mSegsOut);
     }
 
     @Override
     public String toString() {
-        String str = "TcpInfo{ ";
-        for (final Field key : mFieldsValues.keySet()) {
-            str += key.name().toLowerCase() + "=";
-            if (key == Field.STATE) {
-                str += getTcpStateName(mFieldsValues.get(key).intValue()) + " ";
-            } else if (key == Field.WSCALE) {
-                str += decodeWscale(mFieldsValues.get(key).byteValue()) + " ";
-            } else {
-                str += mFieldsValues.get(key) + " ";
-            }
-        }
-        str += "}";
-        return str;
+        return "TcpInfo{lost=" + mLost + ", retransmit=" + mRetransmits + ", received=" + mSegsIn
+                + ", sent=" + mSegsOut + "}";
     }
 }
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java
index 78813bd..f660f81 100644
--- a/src/com/android/networkstack/netlink/TcpSocketTracker.java
+++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -340,16 +340,16 @@
             return null;
         }
 
-        stat.sentCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue();
-        stat.receivedCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue();
-        stat.lostCount = current.tcpInfo.getValue(TcpInfo.Field.LOST).intValue();
-        stat.retransmitCount = current.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue();
+        stat.sentCount = current.tcpInfo.mSegsOut;
+        stat.receivedCount = current.tcpInfo.mSegsIn;
+        stat.lostCount = current.tcpInfo.mLost;
+        stat.retransmitCount = current.tcpInfo.mRetransmits;
 
         if (previous != null && previous.tcpInfo != null) {
-            stat.sentCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue();
-            stat.receivedCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue();
-            stat.lostCount -= previous.tcpInfo.getValue(TcpInfo.Field.LOST).intValue();
-            stat.retransmitCount -= previous.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue();
+            stat.sentCount -= previous.tcpInfo.mSegsOut;
+            stat.receivedCount -= previous.tcpInfo.mSegsIn;
+            stat.lostCount -= previous.tcpInfo.mLost;
+            stat.retransmitCount -= previous.tcpInfo.mRetransmits;
         }
 
         return stat;
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index b782efc..c4f057b 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -67,6 +67,7 @@
     platform_apis: true,
     min_sdk_version: "29",
     test_suites: ["device-tests", "mts"],
+    test_config: "AndroidTest_Coverage.xml",
     defaults: ["NetworkStackIntegrationTestsJniDefaults"],
     static_libs: ["NetworkStackTestsLib", "NetworkStackIntegrationTestsLib"],
     compile_multilib: "both",
diff --git a/tests/integration/AndroidTest_Coverage.xml b/tests/integration/AndroidTest_Coverage.xml
new file mode 100644
index 0000000..e33fa87
--- /dev/null
+++ b/tests/integration/AndroidTest_Coverage.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs coverage tests for NetworkStack">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="NetworkStackCoverageTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="NetworkStackCoverageTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.server.networkstack.coverage" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
index e645a2c..e9384c8 100644
--- a/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
+++ b/tests/unit/src/android/net/shared/ProvisioningConfigurationTest.java
@@ -28,6 +28,7 @@
 import android.net.Network;
 import android.net.StaticIpConfiguration;
 import android.net.apf.ApfCapabilities;
+import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -36,6 +37,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.function.Consumer;
 
 /**
@@ -46,6 +49,17 @@
 public class ProvisioningConfigurationTest {
     private ProvisioningConfiguration mConfig;
 
+    private ScanResultInfo makeScanResultInfo(final String ssid) {
+        final byte[] payload = new byte[] {
+            (byte) 0x00, (byte) 0x17, (byte) 0xF2, (byte) 0x06, (byte) 0x01,
+            (byte) 0x01, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x00,
+        };
+        final ScanResultInfo.InformationElement ie =
+                new ScanResultInfo.InformationElement(0xdd /* vendor specific IE id */,
+                        ByteBuffer.wrap(payload));
+        return new ScanResultInfo(ssid, Collections.singletonList(ie));
+    }
+
     @Before
     public void setUp() {
         mConfig = new ProvisioningConfiguration();
@@ -67,8 +81,9 @@
         mConfig.mNetwork = new Network(321);
         mConfig.mDisplayName = "test_config";
         mConfig.mEnablePreconnection = false;
+        mConfig.mScanResultInfo = makeScanResultInfo("ssid");
         // Any added field must be included in equals() to be tested properly
-        assertFieldCountEquals(13, ProvisioningConfiguration.class);
+        assertFieldCountEquals(14, ProvisioningConfiguration.class);
     }
 
     @Test
@@ -101,6 +116,12 @@
     }
 
     @Test
+    public void testParcelUnparcel_NullScanResultInfo() {
+        mConfig.mScanResultInfo = null;
+        doParcelUnparcelTest();
+    }
+
+    @Test
     public void testParcelUnparcel_WithPreDhcpConnection() {
         mConfig.mEnablePreconnection = true;
         doParcelUnparcelTest();
@@ -136,7 +157,9 @@
         assertNotEqualsAfterChange(c -> c.mDisplayName = "other_test");
         assertNotEqualsAfterChange(c -> c.mDisplayName = null);
         assertNotEqualsAfterChange(c -> c.mEnablePreconnection = true);
-        assertFieldCountEquals(13, ProvisioningConfiguration.class);
+        assertNotEqualsAfterChange(c -> c.mScanResultInfo = null);
+        assertNotEqualsAfterChange(c -> c.mScanResultInfo = makeScanResultInfo("another ssid"));
+        assertFieldCountEquals(14, ProvisioningConfiguration.class);
     }
 
     private void assertNotEqualsAfterChange(Consumer<ProvisioningConfiguration> mutator) {
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
index f65de9c..ddab8c7 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
@@ -89,6 +89,8 @@
             "0000000000000000";   // sndBufLimited = 0
     private static final byte[] TCP_INFO_BYTES =
             HexEncoding.decode(TCP_INFO_HEX.toCharArray(), false);
+    private static final TcpInfo TEST_TCPINFO =
+            new TcpInfo(0 /* retransmits */, 0 /* lost */, 2 /* segsOut */, 1 /* segsIn */);
 
     private static final String EXPANDED_TCP_INFO_HEX = TCP_INFO_HEX
             + "00000000"         // tcpi_delivered
@@ -100,40 +102,48 @@
     @Test
     public void testParseTcpInfo() {
         final ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);
-        final Map<TcpInfo.Field, Number> expected = makeTestTcpInfoHash();
-        final TcpInfo parsedInfo = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
+        // Length is less than required
+        final TcpInfo nullInfo = TcpInfo.parse(buffer, SHORT_TEST_TCP_INFO);
+        assertEquals(nullInfo, null);
 
-        assertEquals(parsedInfo, new TcpInfo(expected));
+        final TcpInfo parsedInfo = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
+        assertEquals(parsedInfo, TEST_TCPINFO);
+
+        // Make a data that TcpInfo is not started from the begining of the buffer.
+        final ByteBuffer bufferWithHeader =
+                ByteBuffer.allocate(EXPANDED_TCP_INFO_BYTES.length + TCP_INFO_BYTES.length);
+        bufferWithHeader.put(EXPANDED_TCP_INFO_BYTES);
+        bufferWithHeader.put(TCP_INFO_BYTES);
+        final TcpInfo infoWithHeader = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
+        bufferWithHeader.position(EXPANDED_TCP_INFO_BYTES.length);
+        assertEquals(parsedInfo, TEST_TCPINFO);
+    }
+
+    @Test
+    public void testFieldOffset() {
+        assertEquals(TcpInfo.RETRANSMITS_OFFSET, 2);
+        assertEquals(TcpInfo.LOST_OFFSET, 32);
+        assertEquals(TcpInfo.SEGS_OUT_OFFSET, 136);
+        assertEquals(TcpInfo.SEGS_IN_OFFSET, 140);
     }
 
     @Test
     public void testParseTcpInfoExpanded() {
         final ByteBuffer buffer = ByteBuffer.wrap(EXPANDED_TCP_INFO_BYTES);
-        final Map<TcpInfo.Field, Number> expected = makeTestTcpInfoHash();
         final TcpInfo parsedInfo =
                 TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1 + EXPANDED_TCP_INFO_LENGTH);
 
-        assertEquals(parsedInfo, new TcpInfo(expected));
+        assertEquals(parsedInfo, TEST_TCPINFO);
         assertEquals(buffer.limit(), buffer.position());
 
         // reset the index.
         buffer.position(0);
         final TcpInfo parsedInfoShorterLen = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
-        assertEquals(parsedInfoShorterLen, new TcpInfo(expected));
+        assertEquals(parsedInfoShorterLen, TEST_TCPINFO);
         assertEquals(TCP_INFO_LENGTH_V1, buffer.position());
     }
 
     @Test
-    public void testValidOffset() {
-        final ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);
-
-        final Map<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();
-        final TcpInfo parsedInfo = TcpInfo.parse(buffer, SHORT_TEST_TCP_INFO);
-
-        assertEquals(parsedInfo, new TcpInfo(expected));
-    }
-
-    @Test
     public void testTcpStateName() {
         assertEquals(TcpInfo.getTcpStateName(4), TCP_FIN_WAIT1);
         assertEquals(TcpInfo.getTcpStateName(1), TCP_ESTABLISHED);
@@ -156,39 +166,14 @@
     @Test
     public void testMalformedTcpInfo() {
         final ByteBuffer buffer = ByteBuffer.wrap(MALFORMED_TCP_INFO_BYTES);
-        final Map<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();
 
         TcpInfo parsedInfo = TcpInfo.parse(buffer, SHORT_TEST_TCP_INFO);
-        assertEquals(parsedInfo, new TcpInfo(expected));
+        assertEquals(parsedInfo, null);
 
         parsedInfo = TcpInfo.parse(buffer, TCP_INFO_LENGTH_V1);
         assertEquals(parsedInfo, null);
     }
 
-    @Test
-    public void testGetValue() {
-        ByteBuffer buffer = ByteBuffer.wrap(TCP_INFO_BYTES);
-
-        final Map<TcpInfo.Field, Number> expected = makeShortTestTcpInfoHash();
-        expected.put(TcpInfo.Field.MAX_PACING_RATE, 10_000L);
-        expected.put(TcpInfo.Field.FACKETS, 10);
-
-        final TcpInfo expectedInfo = new TcpInfo(expected);
-        assertEquals((byte) 0x01, expectedInfo.getValue(TcpInfo.Field.STATE));
-        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.CASTATE));
-        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.RETRANSMITS));
-        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.PROBES));
-        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.BACKOFF));
-        assertEquals((byte) 0x07, expectedInfo.getValue(TcpInfo.Field.OPTIONS));
-        assertEquals((byte) 0x88, expectedInfo.getValue(TcpInfo.Field.WSCALE));
-        assertEquals((byte) 0x00, expectedInfo.getValue(TcpInfo.Field.DELIVERY_RATE_APP_LIMITED));
-
-        assertEquals(10_000L, expectedInfo.getValue(TcpInfo.Field.MAX_PACING_RATE));
-        assertEquals(10, expectedInfo.getValue(TcpInfo.Field.FACKETS));
-        assertEquals(null, expectedInfo.getValue(TcpInfo.Field.RTT));
-
-    }
-
     // Make a TcpInfo contains only first 8 bytes.
     private Map<TcpInfo.Field, Number> makeShortTestTcpInfoHash() {
         final Map<TcpInfo.Field, Number> info = new LinkedHashMap<>();
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
index a21e7cf..6a09f12 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
@@ -61,7 +61,6 @@
 import java.io.FileDescriptor;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.util.HashMap;
 
 // TODO: Add more tests for missing coverage.
 @RunWith(AndroidJUnit4.class)
@@ -174,6 +173,8 @@
             "0000000000000000"; // deliverRate = 0
     private static final byte[] SOCK_DIAG_TCP_INET_BYTES =
             HexEncoding.decode(SOCK_DIAG_TCP_INET_HEX.toCharArray(), false);
+    private static final TcpInfo TEST_TCPINFO =
+            new TcpInfo(5 /* retransmits */, 0 /* lost */, 10 /* segsOut */, 0 /* segsIn */);
 
     private static final String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_INET_HEX
             // struct nlmsghdr
@@ -253,52 +254,8 @@
         buffer.position(SOCKDIAG_MSG_HEADER_SIZE);
         final TcpSocketTracker.SocketInfo parsed =
                 tst.parseSockInfo(buffer, AF_INET, 276, 100L);
-        final HashMap<TcpInfo.Field, Number> expected = new HashMap<>();
-        expected.put(TcpInfo.Field.STATE, (byte) 0x01);
-        expected.put(TcpInfo.Field.CASTATE, (byte) 0x00);
-        expected.put(TcpInfo.Field.RETRANSMITS, (byte) 0x05);
-        expected.put(TcpInfo.Field.PROBES, (byte) 0x00);
-        expected.put(TcpInfo.Field.BACKOFF, (byte) 0x00);
-        expected.put(TcpInfo.Field.OPTIONS, (byte) 0x07);
-        expected.put(TcpInfo.Field.WSCALE, (byte) 0x88);
-        expected.put(TcpInfo.Field.DELIVERY_RATE_APP_LIMITED, (byte) 0x00);
-        expected.put(TcpInfo.Field.RTO, 1806666);
-        expected.put(TcpInfo.Field.ATO, 0);
-        expected.put(TcpInfo.Field.SND_MSS, 1326);
-        expected.put(TcpInfo.Field.RCV_MSS, 536);
-        expected.put(TcpInfo.Field.UNACKED, 0);
-        expected.put(TcpInfo.Field.SACKED, 0);
-        expected.put(TcpInfo.Field.LOST, 0);
-        expected.put(TcpInfo.Field.RETRANS, 0);
-        expected.put(TcpInfo.Field.FACKETS, 0);
-        expected.put(TcpInfo.Field.LAST_DATA_SENT, 187);
-        expected.put(TcpInfo.Field.LAST_ACK_SENT, 0);
-        expected.put(TcpInfo.Field.LAST_DATA_RECV, 187);
-        expected.put(TcpInfo.Field.LAST_ACK_RECV, 187);
-        expected.put(TcpInfo.Field.PMTU, 1500);
-        expected.put(TcpInfo.Field.RCV_SSTHRESH, 87600);
-        expected.put(TcpInfo.Field.RTT, 601150);
-        expected.put(TcpInfo.Field.RTTVAR, 300575);
-        expected.put(TcpInfo.Field.SND_SSTHRESH, 1400);
-        expected.put(TcpInfo.Field.SND_CWND, 10);
-        expected.put(TcpInfo.Field.ADVMSS, 1448);
-        expected.put(TcpInfo.Field.REORDERING, 3);
-        expected.put(TcpInfo.Field.RCV_RTT, 0);
-        expected.put(TcpInfo.Field.RCV_SPACE, 87600);
-        expected.put(TcpInfo.Field.TOTAL_RETRANS, 0);
-        expected.put(TcpInfo.Field.PACING_RATE, 44115L);
-        expected.put(TcpInfo.Field.MAX_PACING_RATE, -1L);
-        expected.put(TcpInfo.Field.BYTES_ACKED, 1L);
-        expected.put(TcpInfo.Field.BYTES_RECEIVED, 0L);
-        expected.put(TcpInfo.Field.SEGS_OUT, 10);
-        expected.put(TcpInfo.Field.SEGS_IN, 0);
-        expected.put(TcpInfo.Field.NOTSENT_BYTES, 0);
-        expected.put(TcpInfo.Field.MIN_RTT, 601150);
-        expected.put(TcpInfo.Field.DATA_SEGS_IN, 0);
-        expected.put(TcpInfo.Field.DATA_SEGS_OUT, 0);
-        expected.put(TcpInfo.Field.DELIVERY_RATE, 0L);
 
-        assertEquals(parsed.tcpInfo, new TcpInfo(expected));
+        assertEquals(parsed.tcpInfo, TEST_TCPINFO);
         assertEquals(parsed.fwmark, 789125);
         assertEquals(parsed.updateTime, 100);
         assertEquals(parsed.ipFamily, AF_INET);
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index e2c0b04..157d257 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -130,7 +130,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -232,6 +231,9 @@
     private static final NetworkCapabilities NO_INTERNET_CAPABILITIES = new NetworkCapabilities()
             .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
 
+    private static final int NOTIFY_NETWORK_TESTED_VALIDATION_RESULT_MASK = 0x3;
+    private static final int NOTIFY_NETWORK_TESTED_SUCCESSFUL_PROBES_MASK = 0xFFFC;
+
     /**
      * Fakes DNS responses.
      *
@@ -795,7 +797,7 @@
         makeDnsTimeoutEvent(wrappedMonitor, DEFAULT_DNS_TIMEOUT_THRESHOLD);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_DNS_EVENTS),
-                argThat(getDataStallDnsBundleMatcher()));
+                bundleForDnsDataStall(DEFAULT_DNS_TIMEOUT_THRESHOLD));
     }
 
     @Test
@@ -808,7 +810,7 @@
         makeDnsTimeoutEvent(wrappedMonitor, DEFAULT_DNS_TIMEOUT_THRESHOLD);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_DNS_EVENTS),
-                argThat(getDataStallDnsBundleMatcher()));
+                bundleForDnsDataStall(DEFAULT_DNS_TIMEOUT_THRESHOLD));
     }
 
     @Test
@@ -824,8 +826,10 @@
 
         makeDnsTimeoutEvent(wrappedMonitor, 3);
         assertTrue(wrappedMonitor.isDataStall());
+
+        // The expected timeout count is the previous 2 DNS timeouts + the most recent 3 timeouts
         verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_DNS_EVENTS),
-                argThat(getDataStallDnsBundleMatcher()));
+                bundleForDnsDataStall(5));
 
         // Set the value to larger than the default dns log size.
         setConsecutiveDnsTimeoutThreshold(51);
@@ -836,8 +840,10 @@
 
         makeDnsTimeoutEvent(wrappedMonitor, 1);
         assertTrue(wrappedMonitor.isDataStall());
-        verify(mCallbacks, times(2)).notifyDataStallSuspected(anyLong(),
-                eq(DETECTION_METHOD_DNS_EVENTS), argThat(getDataStallDnsBundleMatcher()));
+
+        // The expected timeout count is the previous 50 DNS timeouts + the most recent timeout
+        verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_DNS_EVENTS),
+                bundleForDnsDataStall(51));
     }
 
     @Test
@@ -863,7 +869,7 @@
         wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_DNS_EVENTS),
-                argThat(getDataStallDnsBundleMatcher()));
+                bundleForDnsDataStall(DEFAULT_DNS_TIMEOUT_THRESHOLD));
 
         // Test dns events happened before valid dns time threshold.
         setValidDataStallDnsTimeThreshold(0);
@@ -898,7 +904,7 @@
         HandlerUtilsKt.waitForIdle(wrappedMonitor.getHandler(), HANDLER_TIMEOUT_MS);
         assertTrue(wrappedMonitor.isDataStall());
         verify(mCallbacks).notifyDataStallSuspected(anyLong(), eq(DETECTION_METHOD_TCP_METRICS),
-                argThat(getDataStallTcpBundleMatcher()));
+                bundleForTcpDataStall());
     }
 
     @Test
@@ -961,12 +967,13 @@
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);
 
+        final int expectedResult = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP
+                | NETWORK_VALIDATION_RESULT_VALID;
         resetCallbacks();
         nm.notifyCaptivePortalAppFinished(APP_RETURN_DISMISSED);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).atLeastOnce())
-                .notifyNetworkTestedWithExtras(eq(NETWORK_VALIDATION_PROBE_DNS
-                        | NETWORK_VALIDATION_PROBE_HTTP | NETWORK_VALIDATION_RESULT_VALID), any(),
-                        anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                .notifyNetworkTestedWithExtras(eq(expectedResult), any(), anyLong(),
+                        bundleForNotifyNetworkTested(expectedResult));
         assertEquals(0, mRegisteredReceivers.size());
     }
 
@@ -975,6 +982,8 @@
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);
 
+        final int expectedResult = VALIDATION_RESULT_VALID | NETWORK_VALIDATION_PROBE_PRIVDNS;
+
         // Verify dns query only get v6 address.
         mFakeDns.setAnswer("dns6.google", new String[]{"2001:db8::53"}, TYPE_AAAA);
         WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor();
@@ -982,9 +991,8 @@
                 new InetAddress[0]));
         wnm.notifyNetworkConnected(TEST_LINK_PROPERTIES, NOT_METERED_CAPABILITIES);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
-                .notifyNetworkTestedWithExtras(eq(VALIDATION_RESULT_VALID
-                        | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                .notifyNetworkTestedWithExtras(eq(expectedResult), eq(null), anyLong(),
+                        bundleForNotifyNetworkTested(expectedResult));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(VALIDATION_RESULT_PRIVDNS_VALID));
 
@@ -994,9 +1002,8 @@
         wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns4.google",
                 new InetAddress[0]));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
-                .notifyNetworkTestedWithExtras(eq(VALIDATION_RESULT_VALID
-                        | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                .notifyNetworkTestedWithExtras(eq(expectedResult), eq(null), anyLong(),
+                        bundleForNotifyNetworkTested(expectedResult));
         // NetworkMonitor will check if the probes has changed or not, if the probes has not
         // changed, the callback won't be fired.
         verify(mCallbacks, never()).notifyProbeStatusChanged(
@@ -1008,9 +1015,8 @@
         mFakeDns.setAnswer("dns.google", new String[]{"192.0.2.3"}, TYPE_A);
         wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0]));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
-                .notifyNetworkTestedWithExtras(eq(VALIDATION_RESULT_VALID
-                        | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                .notifyNetworkTestedWithExtras(eq(expectedResult), eq(null), anyLong(),
+                        bundleForNotifyNetworkTested(expectedResult));
         verify(mCallbacks, never()).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(VALIDATION_RESULT_PRIVDNS_VALID));
     }
@@ -1026,7 +1032,8 @@
         wnm.notifyNetworkConnected(TEST_LINK_PROPERTIES, NOT_METERED_CAPABILITIES);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyNetworkTestedWithExtras(
                 eq(NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS), eq(null),
-                anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                anyLong(), bundleForNotifyNetworkTested(NETWORK_VALIDATION_PROBE_DNS
+                | NETWORK_VALIDATION_PROBE_HTTPS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(NETWORK_VALIDATION_PROBE_DNS
                 | NETWORK_VALIDATION_PROBE_HTTPS));
@@ -1041,7 +1048,8 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
                 .notifyNetworkTestedWithExtras(eq(VALIDATION_RESULT_VALID
                         | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                        bundleForNotifyNetworkTested(VALIDATION_RESULT_VALID
+                        | NETWORK_VALIDATION_PROBE_PRIVDNS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(VALIDATION_RESULT_PRIVDNS_VALID));
     }
@@ -1057,7 +1065,8 @@
         wnm.notifyNetworkConnected(TEST_LINK_PROPERTIES, NOT_METERED_CAPABILITIES);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS)).notifyNetworkTestedWithExtras(
                 eq(NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS), eq(null),
-                anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                anyLong(), bundleForNotifyNetworkTested(NETWORK_VALIDATION_PROBE_DNS
+                | NETWORK_VALIDATION_PROBE_HTTPS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(NETWORK_VALIDATION_PROBE_DNS
                 | NETWORK_VALIDATION_PROBE_HTTPS));
@@ -1070,7 +1079,8 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).atLeastOnce())
                 .notifyNetworkTestedWithExtras(eq(VALIDATION_RESULT_VALID
                         | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                        bundleForNotifyNetworkTested(VALIDATION_RESULT_VALID
+                        | NETWORK_VALIDATION_PROBE_PRIVDNS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(VALIDATION_RESULT_PRIVDNS_VALID));
 
@@ -1084,7 +1094,8 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS))
                 .notifyNetworkTestedWithExtras(eq(NETWORK_VALIDATION_PROBE_DNS
                         | NETWORK_VALIDATION_PROBE_HTTPS), eq(null), anyLong(),
-                        argThat(getNotifyNetworkTestedBundleMatcher()));
+                        bundleForNotifyNetworkTested(NETWORK_VALIDATION_PROBE_DNS
+                        | NETWORK_VALIDATION_PROBE_HTTPS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(NETWORK_VALIDATION_PROBE_DNS
                 | NETWORK_VALIDATION_PROBE_HTTPS));
@@ -1097,7 +1108,8 @@
                 new InetAddress[0]));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS)).notifyNetworkTestedWithExtras(
                 eq(NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS), eq(null),
-                anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                anyLong(), bundleForNotifyNetworkTested(NETWORK_VALIDATION_PROBE_DNS
+                | NETWORK_VALIDATION_PROBE_HTTPS));
         // NetworkMonitor will check if the probes has changed or not, if the probes has not
         // changed, the callback won't be fired.
         verify(mCallbacks, never()).notifyProbeStatusChanged(
@@ -1111,7 +1123,8 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).atLeastOnce())
                 .notifyNetworkTestedWithExtras(
                         eq(VALIDATION_RESULT_VALID | NETWORK_VALIDATION_PROBE_PRIVDNS), eq(null),
-                        anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                        anyLong(), bundleForNotifyNetworkTested(VALIDATION_RESULT_VALID
+                        | NETWORK_VALIDATION_PROBE_PRIVDNS));
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyProbeStatusChanged(
                 eq(VALIDATION_RESULT_PRIVDNS_VALID), eq(VALIDATION_RESULT_PRIVDNS_VALID));
     }
@@ -1176,12 +1189,13 @@
         // Expect to send HTTP, HTTPS, FALLBACK probe and evaluation result notifications to CS.
         final NetworkMonitor nm = runNetworkTest(VALIDATION_RESULT_PARTIAL);
 
+        final int expectedResult = VALIDATION_RESULT_PARTIAL | NETWORK_VALIDATION_RESULT_VALID;
         resetCallbacks();
         nm.setAcceptPartialConnectivity();
         // Expect to update evaluation result notifications to CS.
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyNetworkTestedWithExtras(
-                eq(VALIDATION_RESULT_PARTIAL | NETWORK_VALIDATION_RESULT_VALID), eq(null),
-                anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                eq(expectedResult), eq(null), anyLong(), bundleForNotifyNetworkTested(
+                expectedResult));
     }
 
     @Test
@@ -1245,6 +1259,9 @@
 
     @Test
     public void testNotifyNetwork_WithforceReevaluation() throws Exception {
+        final int expectedResult = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK
+                | NETWORK_VALIDATION_RESULT_PARTIAL;
+
         final NetworkMonitor nm = runValidatedNetworkTest();
         // Verify forceReevalution will not reset the validation result but only probe result until
         // getting the validation result.
@@ -1256,9 +1273,8 @@
         final ArgumentCaptor<Integer> intCaptor = ArgumentCaptor.forClass(Integer.class);
         // Expect to send HTTP, HTTPs, FALLBACK and evaluation results.
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS))
-                .notifyNetworkTestedWithExtras(eq(NETWORK_VALIDATION_PROBE_DNS
-                        | NETWORK_VALIDATION_PROBE_FALLBACK | NETWORK_VALIDATION_RESULT_PARTIAL),
-                        any(), anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                .notifyNetworkTestedWithExtras(eq(expectedResult), any(), anyLong(),
+                        bundleForNotifyNetworkTested(expectedResult));
     }
 
     @Test
@@ -1313,20 +1329,23 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyNetworkTestedWithExtras(
                 eq(NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
                 | NETWORK_VALIDATION_RESULT_PARTIAL), eq(null), anyLong(),
-                argThat(getNotifyNetworkTestedBundleMatcher()));
+                bundleForNotifyNetworkTested(NETWORK_VALIDATION_PROBE_DNS
+                | NETWORK_VALIDATION_PROBE_HTTPS | NETWORK_VALIDATION_RESULT_PARTIAL));
 
         nm.getEvaluationState().reportEvaluationResult(
                 NETWORK_VALIDATION_RESULT_VALID | NETWORK_VALIDATION_RESULT_PARTIAL,
                 null /* redirectUrl */);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyNetworkTestedWithExtras(
                 eq(VALIDATION_RESULT_VALID | NETWORK_VALIDATION_RESULT_PARTIAL), eq(null),
-                anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                anyLong(), bundleForNotifyNetworkTested(VALIDATION_RESULT_VALID
+                | NETWORK_VALIDATION_RESULT_PARTIAL));
 
         nm.getEvaluationState().reportEvaluationResult(VALIDATION_RESULT_INVALID,
                 TEST_REDIRECT_URL);
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyNetworkTestedWithExtras(
                 eq(NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS),
-                eq(TEST_REDIRECT_URL), anyLong(), argThat(getNotifyNetworkTestedBundleMatcher()));
+                eq(TEST_REDIRECT_URL), anyLong(), bundleForNotifyNetworkTested(
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS));
     }
 
     @Test
@@ -1482,7 +1501,7 @@
             verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS))
                     .notifyNetworkTestedWithExtras(eq(testResult),
                             mNetworkTestedRedirectUrlCaptor.capture(), anyLong(),
-                            argThat(getNotifyNetworkTestedBundleMatcher()));
+                            bundleForNotifyNetworkTested(testResult));
         } catch (RemoteException e) {
             fail("Unexpected exception: " + e);
         }
@@ -1514,21 +1533,32 @@
         }
     }
 
-    private ArgumentMatcher<PersistableBundle> getNotifyNetworkTestedBundleMatcher() {
-        return bundle ->
+    private PersistableBundle bundleForNotifyNetworkTested(final int result) {
+        // result = KEY_NETWORK_PROBES_SUCCEEDED_BITMASK | KEY_NETWORK_VALIDATION_RESULT
+        // See NetworkMonitor.EvaluationState#getNetworkTestResult
+        final int validationResult = result & NOTIFY_NETWORK_TESTED_VALIDATION_RESULT_MASK;
+        final int probesSucceeded = result & NOTIFY_NETWORK_TESTED_SUCCESSFUL_PROBES_MASK;
+
+        return argThat(bundle ->
                 bundle.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK)
                 && bundle.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK)
-                && bundle.containsKey(KEY_NETWORK_VALIDATION_RESULT);
+                && (bundle.getInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK) & probesSucceeded)
+                        == probesSucceeded
+                && bundle.containsKey(KEY_NETWORK_VALIDATION_RESULT)
+                && (bundle.getInt(KEY_NETWORK_VALIDATION_RESULT) & validationResult)
+                        == validationResult);
     }
 
-    private ArgumentMatcher<PersistableBundle> getDataStallDnsBundleMatcher() {
-        return bundle -> bundle.containsKey(KEY_DNS_CONSECUTIVE_TIMEOUTS);
+    private PersistableBundle bundleForDnsDataStall(final int timeoutCount) {
+        return argThat(bundle ->
+                bundle.containsKey(KEY_DNS_CONSECUTIVE_TIMEOUTS)
+                && bundle.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS) == timeoutCount);
     }
 
-    private ArgumentMatcher<PersistableBundle> getDataStallTcpBundleMatcher() {
-        return bundle ->
+    private PersistableBundle bundleForTcpDataStall() {
+        return argThat(bundle ->
                 bundle.containsKey(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS)
-                && bundle.containsKey(KEY_TCP_PACKET_FAIL_RATE);
+                && bundle.containsKey(KEY_TCP_PACKET_FAIL_RATE));
     }
 }