Merge "add TCP data stall metrics" into rvc-dev am: 92fbcb262a am: 797bcc347e

Change-Id: Ib5bedf6daea17d09e10278dc4bb452ff3610e7f1
diff --git a/src/android/net/util/DataStallUtils.java b/src/android/net/util/DataStallUtils.java
index 5787879..3391a71 100644
--- a/src/android/net/util/DataStallUtils.java
+++ b/src/android/net/util/DataStallUtils.java
@@ -16,15 +16,30 @@
 
 package android.net.util;
 
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Collection of utilities for data stall.
  */
 public class DataStallUtils {
+    public static final int DATA_STALL_EVALUATION_TYPE_NONE = 0;
     /** Detect data stall using dns timeout counts. */
     public static final int DATA_STALL_EVALUATION_TYPE_DNS = 1 << 0;
     /** Detect data stall using tcp connection fail rate. */
     public static final int DATA_STALL_EVALUATION_TYPE_TCP = 1 << 1;
 
+    @IntDef(prefix = { "DATA_STALL_EVALUATION_TYPE_" }, value = {
+        DATA_STALL_EVALUATION_TYPE_NONE,
+        DATA_STALL_EVALUATION_TYPE_DNS,
+        DATA_STALL_EVALUATION_TYPE_TCP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EvaluationType {
+    }
+
     // Default configuration values for data stall detection.
     public static final int DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD = 5;
     public static final int DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS = 60 * 1000;
diff --git a/src/com/android/networkstack/metrics/DataStallDetectionStats.java b/src/com/android/networkstack/metrics/DataStallDetectionStats.java
index 7a1f9ac..d294e04 100644
--- a/src/com/android/networkstack/metrics/DataStallDetectionStats.java
+++ b/src/com/android/networkstack/metrics/DataStallDetectionStats.java
@@ -19,8 +19,10 @@
 import android.net.util.NetworkStackUtils;
 import android.net.wifi.WifiInfo;
 
+import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.util.HexDump;
 import com.android.server.connectivity.nano.CellularData;
@@ -41,18 +43,35 @@
  * @hide
  */
 public final class DataStallDetectionStats {
-    private static final int UNKNOWN_SIGNAL_STRENGTH = -1;
+    public static final int UNKNOWN_SIGNAL_STRENGTH = -1;
+    // Default value of TCP signals.
+    @VisibleForTesting
+    public static final int UNSPECIFIED_TCP_FAIL_RATE = -1;
+    @VisibleForTesting
+    public static final int UNSPECIFIED_TCP_PACKETS_COUNT = -1;
+    @VisibleForTesting
     @NonNull
-    final byte[] mCellularInfo;
+    public final byte[] mCellularInfo;
+    @VisibleForTesting
     @NonNull
-    final byte[] mWifiInfo;
+    public final byte[] mWifiInfo;
     @NonNull
-    final byte[] mDns;
-    final int mEvaluationType;
-    final int mNetworkType;
+    public final byte[] mDns;
+    @VisibleForTesting
+    public final int mEvaluationType;
+    @VisibleForTesting
+    public final int mNetworkType;
+    // The TCP packets fail rate percentage from the latest tcp polling. -1 means the TCP signal is
+    // not known or not supported on the SDK version of this device.
+    @VisibleForTesting @IntRange(from = -1, to = 100)
+    public final int mTcpFailRate;
+    // Number of packets sent since the last received packet.
+    @VisibleForTesting
+    public final int mTcpSentSinceLastRecv;
 
     public DataStallDetectionStats(@Nullable byte[] cell, @Nullable byte[] wifi,
-                @NonNull int[] returnCode, @NonNull long[] dnsTime, int evalType, int netType) {
+                @NonNull int[] returnCode, @NonNull long[] dnsTime, int evalType, int netType,
+                int failRate, int sentSinceLastRecv) {
         mCellularInfo = emptyCellDataIfNull(cell);
         mWifiInfo = emptyWifiInfoIfNull(wifi);
 
@@ -62,9 +81,20 @@
         mDns = MessageNano.toByteArray(dns);
         mEvaluationType = evalType;
         mNetworkType = netType;
+        mTcpFailRate = failRate;
+        mTcpSentSinceLastRecv = sentSinceLastRecv;
     }
 
-    private byte[] emptyCellDataIfNull(@Nullable byte[] cell) {
+    /**
+     * Because metrics data must contain data for each field even if it's not supported or not
+     * available, generate a byte array representing an empty {@link CellularData} if the
+     * {@link CellularData} is unavailable.
+     *
+     * @param cell a byte array representing current {@link CellularData} of {@code this}
+     * @return a byte array of a {@link CellularData}.
+     */
+    @VisibleForTesting
+    public static byte[] emptyCellDataIfNull(@Nullable byte[] cell) {
         if (cell != null) return cell;
 
         CellularData data  = new CellularData();
@@ -75,7 +105,16 @@
         return MessageNano.toByteArray(data);
     }
 
-    private byte[] emptyWifiInfoIfNull(@Nullable byte[] wifi) {
+    /**
+     * Because metrics data must contain data for each field even if it's not supported or not
+     * available, generate a byte array representing an empty {@link WifiData} if the
+     * {@link WiFiData} is unavailable.
+     *
+     * @param wifi a byte array representing current {@link WiFiData} of {@code this}.
+     * @return a byte array of a {@link WiFiData}.
+     */
+    @VisibleForTesting
+    public static byte[] emptyWifiInfoIfNull(@Nullable byte[] wifi) {
         if (wifi != null) return wifi;
 
         WifiData data = new WifiData();
@@ -95,7 +134,11 @@
           .append(", cell info: ")
           .append(HexDump.toHexString(mCellularInfo))
           .append(", dns: ")
-          .append(HexDump.toHexString(mDns));
+          .append(HexDump.toHexString(mDns))
+          .append(", tcp fail rate: ")
+          .append(mTcpFailRate)
+          .append(", tcp received: ")
+          .append(mTcpSentSinceLastRecv);
         return sb.toString();
     }
 
@@ -107,12 +150,15 @@
             && (mEvaluationType == other.mEvaluationType)
             && Arrays.equals(mWifiInfo, other.mWifiInfo)
             && Arrays.equals(mCellularInfo, other.mCellularInfo)
-            && Arrays.equals(mDns, other.mDns);
+            && Arrays.equals(mDns, other.mDns)
+            && (mTcpFailRate == other.mTcpFailRate)
+            && (mTcpSentSinceLastRecv == other.mTcpSentSinceLastRecv);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mNetworkType, mEvaluationType, mWifiInfo, mCellularInfo, mDns);
+        return Objects.hash(mNetworkType, mEvaluationType, mWifiInfo, mCellularInfo, mDns,
+                mTcpFailRate, mTcpSentSinceLastRecv);
     }
 
     /**
@@ -131,6 +177,8 @@
         private final List<Long> mDnsTimeStamp = new ArrayList<Long>();
         private int mEvaluationType;
         private int mNetworkType;
+        private int mTcpFailRate = UNSPECIFIED_TCP_FAIL_RATE;
+        private int mTcpSentSinceLastRecv = UNSPECIFIED_TCP_PACKETS_COUNT;
 
         /**
          * Add a dns event into Builder.
@@ -168,6 +216,34 @@
         }
 
         /**
+         * Set the TCP packet fail rate into Builder. The data is included since android R.
+         *
+         * @param rate the TCP packet fail rate of the logged network. The default value is
+         *               {@code UNSPECIFIED_TCP_FAIL_RATE}, which means the TCP signal is not known
+         *               or not supported on the SDK version of this device.
+         * @return {@code this} {@link Builder} instance.
+         */
+        public Builder setTcpFailRate(@IntRange(from = -1, to = 100) int rate) {
+            mTcpFailRate = rate;
+            return this;
+        }
+
+        /**
+         * Set the number of TCP packets sent since the last received packet into Builder. The data
+         * starts to be included since android R.
+         *
+         * @param count the number of packets sent since the last received packet of the logged
+         *              network. Keep it unset as default value or set to
+         *              {@code UNSPECIFIED_TCP_PACKETS_COUNT} if the tcp signal is unsupported with
+         *              current device android sdk version or the packets count is unknown.
+         * @return {@code this} {@link Builder} instance.
+         */
+        public Builder setTcpSentSinceLastRecv(int count) {
+            mTcpSentSinceLastRecv = count;
+            return this;
+        }
+
+        /**
          * Set the wifi data into Builder.
          *
          * @param info a {@link WifiInfo} of the connected wifi network.
@@ -223,7 +299,7 @@
             return new DataStallDetectionStats(mCellularInfo, mWifiInfo,
                     NetworkStackUtils.convertToIntArray(mDnsReturnCode),
                     NetworkStackUtils.convertToLongArray(mDnsTimeStamp),
-                    mEvaluationType, mNetworkType);
+                    mEvaluationType, mNetworkType, mTcpFailRate, mTcpSentSinceLastRecv);
         }
     }
 }
diff --git a/src/com/android/networkstack/metrics/DataStallStatsUtils.java b/src/com/android/networkstack/metrics/DataStallStatsUtils.java
index e7a6c3d..c0719c0 100644
--- a/src/com/android/networkstack/metrics/DataStallStatsUtils.java
+++ b/src/com/android/networkstack/metrics/DataStallStatsUtils.java
@@ -74,6 +74,8 @@
                 stats.mNetworkType,
                 stats.mWifiInfo,
                 stats.mCellularInfo,
-                stats.mDns);
+                stats.mDns,
+                stats.mTcpFailRate,
+                stats.mTcpSentSinceLastRecv);
     }
 }
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 7fb6761..093118a 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -48,6 +48,7 @@
 import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_TCP_POLLING_INTERVAL;
 import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_VALID_DNS_TIME_THRESHOLD;
 import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_DNS;
+import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_NONE;
 import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_TCP;
 import static android.net.util.DataStallUtils.DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD;
 import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_EVALUATION_TYPES;
@@ -112,6 +113,7 @@
 import android.net.metrics.ValidationProbeEvent;
 import android.net.shared.NetworkMonitorUtils;
 import android.net.shared.PrivateDnsConfig;
+import android.net.util.DataStallUtils.EvaluationType;
 import android.net.util.NetworkStackUtils;
 import android.net.util.SharedLog;
 import android.net.util.Stopwatch;
@@ -489,8 +491,8 @@
     @Nullable
     private final DnsStallDetector mDnsStallDetector;
     private long mLastProbeTime;
-    // Set to true if data stall is suspected and reset to false after metrics are sent to statsd.
-    private boolean mCollectDataStallMetrics;
+    // The signal causing a data stall to be suspected. Reset to 0 after metrics are sent to statsd.
+    private @EvaluationType int mDataStallTypeToCollect;
     private boolean mAcceptPartialConnectivity = false;
     private final EvaluationState mEvaluationState = new EvaluationState();
 
@@ -977,8 +979,6 @@
 
         boolean evaluateDataStall() {
             if (isDataStall()) {
-                // TODO: Add tcp info into metrics.
-                mCollectDataStallMetrics = true;
                 validationLog("Suspecting data stall, reevaluate");
                 return true;
             }
@@ -999,7 +999,8 @@
         }
     }
 
-    private void writeDataStallStats(@NonNull final CaptivePortalProbeResult result) {
+    private void maybeWriteDataStallStats(@NonNull final CaptivePortalProbeResult result) {
+        if (mDataStallTypeToCollect == DATA_STALL_EVALUATION_TYPE_NONE) return;
         /*
          * Collect data stall detection level information for each transport type. Collect type
          * specific information for cellular and wifi only currently. Generate
@@ -1007,19 +1008,22 @@
          * TRANSPORT_WIFI and TRANSPORT_VPN, two DataStallDetectionStats will be generated.
          */
         final int[] transports = mNetworkCapabilities.getTransportTypes();
-
         for (int i = 0; i < transports.length; i++) {
-            final DataStallDetectionStats stats = buildDataStallDetectionStats(transports[i]);
+            final DataStallDetectionStats stats =
+                    buildDataStallDetectionStats(transports[i], mDataStallTypeToCollect);
             mDependencies.writeDataStallDetectionStats(stats, result);
         }
-        mCollectDataStallMetrics = false;
+        mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_NONE;
     }
 
     @VisibleForTesting
-    protected DataStallDetectionStats buildDataStallDetectionStats(int transport) {
+    protected DataStallDetectionStats buildDataStallDetectionStats(int transport,
+            @EvaluationType int evaluationType) {
         final DataStallDetectionStats.Builder stats = new DataStallDetectionStats.Builder();
-        if (VDBG_STALL) log("collectDataStallMetrics: type=" + transport);
-        stats.setEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS);
+        if (VDBG_STALL) {
+            log("collectDataStallMetrics: type=" + transport + ", evaluation=" + evaluationType);
+        }
+        stats.setEvaluationType(evaluationType);
         stats.setNetworkType(transport);
         switch (transport) {
             case NetworkCapabilities.TRANSPORT_WIFI:
@@ -1044,11 +1048,21 @@
                 // No transport type specific information for the other types.
                 break;
         }
+
         addDnsEvents(stats);
+        addTcpStats(stats);
 
         return stats.build();
     }
 
+    private void addTcpStats(@NonNull final DataStallDetectionStats.Builder stats) {
+        final TcpSocketTracker tst = getTcpSocketTracker();
+        if (tst == null) return;
+
+        stats.setTcpSentSinceLastRecv(tst.getSentSinceLastRecv());
+        stats.setTcpFailRate(tst.getLatestPacketFailPercentage());
+    }
+
     @VisibleForTesting
     protected void addDnsEvents(@NonNull final DataStallDetectionStats.Builder stats) {
         final DnsStallDetector dsd = getDnsStallDetector();
@@ -1438,9 +1452,7 @@
                             (CaptivePortalProbeResult) message.obj;
                     mLastProbeTime = SystemClock.elapsedRealtime();
 
-                    if (mCollectDataStallMetrics) {
-                        writeDataStallStats(probeResult);
-                    }
+                    maybeWriteDataStallStats(probeResult);
 
                     if (probeResult.isSuccessful()) {
                         // Transit EvaluatingPrivateDnsState to get to Validated
@@ -1919,7 +1931,8 @@
                 DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS);
     }
 
-    private int getDataStallEvaluationType() {
+    @VisibleForTesting
+    int getDataStallEvaluationType() {
         return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
                 CONFIG_DATA_STALL_EVALUATION_TYPE,
                 DEFAULT_DATA_STALL_EVALUATION_TYPES);
@@ -3140,9 +3153,8 @@
         return mDnsStallDetector;
     }
 
-    @VisibleForTesting
     @Nullable
-    protected TcpSocketTracker getTcpSocketTracker() {
+    private TcpSocketTracker getTcpSocketTracker() {
         return mTcpTracker;
     }
 
@@ -3179,6 +3191,7 @@
                 result = false;
             } else if (tst.isDataStallSuspected()) {
                 result = true;
+                mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_TCP;
 
                 final DataStallReportParcelable p = new DataStallReportParcelable();
                 p.detectionMethod = DETECTION_METHOD_TCP_METRICS;
@@ -3203,6 +3216,7 @@
             if (dsd.isDataStallSuspected(mConsecutiveDnsTimeoutThreshold,
                     mDataStallValidDnsTimeThreshold)) {
                 result = true;
+                mDataStallTypeToCollect = DATA_STALL_EVALUATION_TYPE_DNS;
                 logNetworkEvent(NetworkEvent.NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND);
 
                 final DataStallReportParcelable p = new DataStallReportParcelable();
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index dda7f08..f0a7bfb 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -39,6 +39,7 @@
 import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_VALID_DNS_TIME_THRESHOLD;
 import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_DNS;
 import static android.net.util.DataStallUtils.DATA_STALL_EVALUATION_TYPE_TCP;
+import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_EVALUATION_TYPES;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
@@ -144,10 +145,15 @@
 import com.android.networkstack.metrics.DataStallStatsUtils;
 import com.android.networkstack.netlink.TcpSocketTracker;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
+import com.android.server.connectivity.nano.CellularData;
+import com.android.server.connectivity.nano.DnsEvent;
+import com.android.server.connectivity.nano.WifiData;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.HandlerUtilsKt;
 
+import com.google.protobuf.nano.MessageNano;
+
 import junit.framework.AssertionFailedError;
 
 import org.junit.After;
@@ -227,7 +233,6 @@
     private @Spy Network mCleartextDnsNetwork = new Network(TEST_NETID);
     private @Mock Network mNetwork;
     private @Mock DataStallStatsUtils mDataStallStatsUtils;
-    private @Mock WifiInfo mWifiInfo;
     private @Mock TcpSocketTracker.Dependencies mTstDependencies;
     private @Mock INetd mNetd;
     private @Mock TcpSocketTracker mTst;
@@ -235,6 +240,7 @@
     private HashSet<BroadcastReceiver> mRegisteredReceivers;
     private @Mock Context mMccContext;
     private @Mock Resources mMccResource;
+    private @Mock WifiInfo mWifiInfo;
 
     private static final int TEST_NETID = 4242;
     private static final String TEST_HTTP_URL = "http://www.google.com/gen_204";
@@ -252,10 +258,12 @@
     private static final String TEST_VENUE_INFO_URL = "https://venue.example.com/info";
     private static final String TEST_SPEED_TEST_URL = "https://speedtest.example.com";
     private static final String TEST_MCCMNC = "123456";
-
     private static final String[] TEST_HTTP_URLS = {TEST_HTTP_OTHER_URL1, TEST_HTTP_OTHER_URL2};
     private static final String[] TEST_HTTPS_URLS = {TEST_HTTPS_OTHER_URL1, TEST_HTTPS_OTHER_URL2};
-
+    private static final int TEST_TCP_FAIL_RATE = 99;
+    private static final int TEST_TCP_PACKET_COUNT = 50;
+    private static final long TEST_ELAPSED_TIME_MS = 123456789L;
+    private static final int TEST_SIGNAL_STRENGTH = -100;
     private static final int VALIDATION_RESULT_INVALID = 0;
     private static final int VALIDATION_RESULT_PORTAL = 0;
     private static final String TEST_REDIRECT_URL = "android.com";
@@ -288,6 +296,12 @@
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
 
+    private static final NetworkCapabilities WIFI_NOT_METERED_CAPABILITIES =
+            new NetworkCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+
     private static final NetworkCapabilities CELL_NO_INTERNET_CAPABILITIES =
             new NetworkCapabilities().addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
 
@@ -445,6 +459,10 @@
 
         when(mServiceManager.getNotifier()).thenReturn(mNotifier);
 
+        when(mTelephony.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE);
+        when(mTelephony.getNetworkOperator()).thenReturn(TEST_MCCMNC);
+        when(mTelephony.getSimOperator()).thenReturn(TEST_MCCMNC);
+
         when(mResources.getString(anyInt())).thenReturn("");
         when(mResources.getStringArray(anyInt())).thenReturn(new String[0]);
         doReturn(mConfiguration).when(mResources).getConfiguration();
@@ -557,13 +575,21 @@
         }
     }
 
+    private TcpSocketTracker getTcpSocketTrackerOrNull(NetworkMonitor.Dependencies dp) {
+        return ((dp.getDeviceConfigPropertyInt(
+                NAMESPACE_CONNECTIVITY,
+                CONFIG_DATA_STALL_EVALUATION_TYPE,
+                DEFAULT_DATA_STALL_EVALUATION_TYPES)
+                & DATA_STALL_EVALUATION_TYPE_TCP) != 0) ? mTst : null;
+    }
+
     private class WrappedNetworkMonitor extends NetworkMonitor {
         private long mProbeTime = 0;
         private final ConditionVariable mQuitCv = new ConditionVariable(false);
 
         WrappedNetworkMonitor() {
             super(mContext, mCallbacks, mNetwork, mLogger, mValidationLogger, mServiceManager,
-                    mDependencies, mTst);
+                    mDependencies, getTcpSocketTrackerOrNull(mDependencies));
         }
 
         @Override
@@ -576,13 +602,10 @@
         }
 
         @Override
-        protected TcpSocketTracker getTcpSocketTracker() {
-            return mTst;
-        }
-
-        @Override
         protected void addDnsEvents(@NonNull final DataStallDetectionStats.Builder stats) {
-            generateTimeoutDnsEvent(stats, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+            if ((getDataStallEvaluationType() & DATA_STALL_EVALUATION_TYPE_DNS) != 0) {
+                generateTimeoutDnsEvent(stats, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+            }
         }
 
         @Override
@@ -622,6 +645,11 @@
         return nm;
     }
 
+    private WrappedNetworkMonitor makeWifiNotMeteredNetworkMonitor() {
+        final WrappedNetworkMonitor nm = makeMonitor(WIFI_NOT_METERED_CAPABILITIES);
+        return nm;
+    }
+
     private void setNetworkCapabilities(NetworkMonitor nm, NetworkCapabilities nc) {
         nm.notifyNetworkCapabilitiesChanged(nc);
         HandlerUtilsKt.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
@@ -1728,85 +1756,284 @@
     }
 
     @Test
-    public void testDataStall_StallDnsSuspectedAndSendMetrics() throws Exception {
-        // Connect a VALID network to simulate the data stall detection because data stall
-        // evaluation will only start from validated state.
-        setStatus(mHttpsConnection, 204);
-        WrappedNetworkMonitor wrappedMonitor = makeCellNotMeteredNetworkMonitor();
-        wrappedMonitor.notifyNetworkConnected(TEST_LINK_PROPERTIES, CELL_METERED_CAPABILITIES);
-        verifyNetworkTested(NETWORK_VALIDATION_RESULT_VALID,
-                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
-        // Setup dns data stall signal.
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
-        makeDnsTimeoutEvent(wrappedMonitor, 5);
-        assertTrue(wrappedMonitor.isDataStall());
+    public void testDataStall_StallDnsSuspectedAndSendMetricsOnCell() throws Exception {
+        testDataStall_StallDnsSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_CELLULAR,
+                CELL_METERED_CAPABILITIES);
+    }
+
+    @Test
+    public void testDataStall_StallDnsSuspectedAndSendMetricsOnWifi() throws Exception {
+        testDataStall_StallDnsSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_WIFI,
+                WIFI_NOT_METERED_CAPABILITIES);
+    }
+
+    private void testDataStall_StallDnsSuspectedAndSendMetrics(int transport,
+            NetworkCapabilities nc) throws Exception {
+        // NM suspects data stall from DNS signal and sends data stall metrics.
+        final WrappedNetworkMonitor nm = prepareNetworkMonitorForVerifyDataStall(nc);
+        makeDnsTimeoutEvent(nm, 5);
         // Trigger a dns signal to start evaluate data stall and upload metrics.
-        wrappedMonitor.notifyDnsResponse(RETURN_CODE_DNS_TIMEOUT);
-        // Setup information to prevent null data and cause NPE during testing.
-        when(mTelephony.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE);
-        when(mTelephony.getNetworkOperator()).thenReturn(TEST_MCCMNC);
-        when(mTelephony.getSimOperator()).thenReturn(TEST_MCCMNC);
-        // Verify data sent as expectation.
-        final DataStallDetectionStats stats = wrappedMonitor.buildDataStallDetectionStats(
-                NetworkCapabilities.TRANSPORT_CELLULAR);
-        final ArgumentCaptor<CaptivePortalProbeResult> probeResultCaptor =
-                ArgumentCaptor.forClass(CaptivePortalProbeResult.class);
-        verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(1))
-                .writeDataStallDetectionStats(eq(stats), probeResultCaptor.capture());
-        assertTrue(probeResultCaptor.getValue().isSuccessful());
+        nm.notifyDnsResponse(RETURN_CODE_DNS_TIMEOUT);
+        // Verify data sent as expected.
+        verifySendDataStallDetectionStats(nm, DATA_STALL_EVALUATION_TYPE_DNS, transport);
     }
 
     @Test
     public void testDataStall_NoStallSuspectedAndSendMetrics() throws Exception {
-        // Connect a VALID network to simulate the data stall detection because data stall
-        // evaluation will only start from validated state.
-        setStatus(mHttpsConnection, 204);
-        WrappedNetworkMonitor wrappedMonitor = makeCellNotMeteredNetworkMonitor();
-        wrappedMonitor.notifyNetworkConnected(TEST_LINK_PROPERTIES, CELL_METERED_CAPABILITIES);
-        verifyNetworkTested(NETWORK_VALIDATION_RESULT_VALID,
-                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
+        final WrappedNetworkMonitor nm = prepareNetworkMonitorForVerifyDataStall(
+                CELL_METERED_CAPABILITIES);
         // Setup no data stall dns signal.
-        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
-        makeDnsTimeoutEvent(wrappedMonitor, 3);
-        assertFalse(wrappedMonitor.isDataStall());
+        makeDnsTimeoutEvent(nm, 3);
+        assertFalse(nm.isDataStall());
         // Trigger a dns signal to start evaluate data stall.
-        wrappedMonitor.notifyDnsResponse(RETURN_CODE_DNS_SUCCESS);
+        nm.notifyDnsResponse(RETURN_CODE_DNS_SUCCESS);
         verify(mDependencies, never()).writeDataStallDetectionStats(any(), any());
     }
 
     @Test
-    public void testCollectDataStallMetrics() {
-        WrappedNetworkMonitor wrappedMonitor = makeCellNotMeteredNetworkMonitor();
+    public void testDataStall_StallTcpSuspectedAndSendMetricsOnCell() throws Exception {
+        testDataStall_StallTcpSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_CELLULAR,
+                CELL_METERED_CAPABILITIES);
+    }
 
-        when(mTelephony.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE);
-        when(mTelephony.getNetworkOperator()).thenReturn(TEST_MCCMNC);
-        when(mTelephony.getSimOperator()).thenReturn(TEST_MCCMNC);
+    @Test
+    public void testDataStall_StallTcpSuspectedAndSendMetricsOnWifi() throws Exception {
+        testDataStall_StallTcpSuspectedAndSendMetrics(NetworkCapabilities.TRANSPORT_WIFI,
+                WIFI_NOT_METERED_CAPABILITIES);
+    }
 
-        DataStallDetectionStats.Builder stats =
-                new DataStallDetectionStats.Builder()
-                .setEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS)
-                .setNetworkType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                .setCellData(TelephonyManager.NETWORK_TYPE_LTE /* radioType */,
+    private void testDataStall_StallTcpSuspectedAndSendMetrics(int transport,
+            NetworkCapabilities nc) throws Exception {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        // NM suspects data stall from TCP signal and sends data stall metrics.
+        setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
+        final WrappedNetworkMonitor nm = prepareNetworkMonitorForVerifyDataStall(nc);
+        setupTcpDataStall();
+        // Trigger a tcp event immediately.
+        setTcpPollingInterval(0);
+        nm.sendTcpPollingEvent();
+        verifySendDataStallDetectionStats(nm, DATA_STALL_EVALUATION_TYPE_TCP, transport);
+    }
+
+    private WrappedNetworkMonitor prepareNetworkMonitorForVerifyDataStall(NetworkCapabilities nc)
+            throws Exception {
+        // Connect a VALID network to simulate the data stall detection because data stall
+        // evaluation will only start from validated state.
+        setStatus(mHttpsConnection, 204);
+        final WrappedNetworkMonitor nm;
+        final int[] transports = nc.getTransportTypes();
+        // Though multiple transport types are allowed, use the first transport type for
+        // simplification.
+        switch (transports[0]) {
+            case NetworkCapabilities.TRANSPORT_CELLULAR:
+                nm = makeCellMeteredNetworkMonitor();
+                break;
+            case NetworkCapabilities.TRANSPORT_WIFI:
+                nm = makeWifiNotMeteredNetworkMonitor();
+                setupTestWifiInfo();
+                break;
+            default:
+                nm = null;
+                fail("Undefined transport type");
+        }
+        nm.notifyNetworkConnected(TEST_LINK_PROPERTIES, nc);
+        verifyNetworkTested(NETWORK_VALIDATION_RESULT_VALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
+        nm.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        return nm;
+    }
+
+    private void setupTcpDataStall() {
+        when(mTstDependencies.isTcpInfoParsingSupported()).thenReturn(true);
+        when(mTst.getLatestReceivedCount()).thenReturn(0);
+        when(mTst.getLatestPacketFailPercentage()).thenReturn(TEST_TCP_FAIL_RATE);
+        when(mTst.getSentSinceLastRecv()).thenReturn(TEST_TCP_PACKET_COUNT);
+        when(mTst.isDataStallSuspected()).thenReturn(true);
+        when(mTst.pollSocketsInfo()).thenReturn(true);
+    }
+
+    private void verifySendDataStallDetectionStats(WrappedNetworkMonitor nm, int evalType,
+            int transport) {
+        // Verify data sent as expectated.
+        final ArgumentCaptor<CaptivePortalProbeResult> probeResultCaptor =
+                ArgumentCaptor.forClass(CaptivePortalProbeResult.class);
+        final ArgumentCaptor<DataStallDetectionStats> statsCaptor =
+                ArgumentCaptor.forClass(DataStallDetectionStats.class);
+        verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .writeDataStallDetectionStats(statsCaptor.capture(), probeResultCaptor.capture());
+        assertTrue(nm.isDataStall());
+        assertTrue(probeResultCaptor.getValue().isSuccessful());
+        verifyTestDataStallDetectionStats(evalType, transport, statsCaptor.getValue());
+    }
+
+    private void verifyTestDataStallDetectionStats(int evalType, int transport,
+            DataStallDetectionStats stats) {
+        assertEquals(transport, stats.mNetworkType);
+        switch (transport) {
+            case NetworkCapabilities.TRANSPORT_WIFI:
+                assertArrayEquals(makeTestWifiDataNano(), stats.mWifiInfo);
+                // Expedient way to check stats.mCellularInfo contains the neutral byte array that
+                // is sent to represent a lack of data, as stats.mCellularInfo is not supposed to
+                // contain null.
+                assertArrayEquals(DataStallDetectionStats.emptyCellDataIfNull(null),
+                        stats.mCellularInfo);
+                break;
+            case NetworkCapabilities.TRANSPORT_CELLULAR:
+                // Expedient way to check stats.mWifiInfo contains the neutral byte array that is
+                // sent to represent a lack of data, as stats.mWifiInfo is not supposed to contain
+                // null.
+                assertArrayEquals(DataStallDetectionStats.emptyWifiInfoIfNull(null),
+                        stats.mWifiInfo);
+                assertArrayEquals(makeTestCellDataNano(), stats.mCellularInfo);
+                break;
+            default:
+                // Add other cases.
+                fail("Unexpected transport type");
+        }
+
+        assertEquals(evalType, stats.mEvaluationType);
+        if ((evalType & DATA_STALL_EVALUATION_TYPE_TCP) != 0) {
+            assertEquals(TEST_TCP_FAIL_RATE, stats.mTcpFailRate);
+            assertEquals(TEST_TCP_PACKET_COUNT, stats.mTcpSentSinceLastRecv);
+        } else {
+            assertEquals(DataStallDetectionStats.UNSPECIFIED_TCP_FAIL_RATE, stats.mTcpFailRate);
+            assertEquals(DataStallDetectionStats.UNSPECIFIED_TCP_PACKETS_COUNT,
+                    stats.mTcpSentSinceLastRecv);
+        }
+
+        if ((evalType & DATA_STALL_EVALUATION_TYPE_DNS) != 0) {
+            assertArrayEquals(stats.mDns, makeTestDnsTimeoutNano(DEFAULT_DNS_TIMEOUT_THRESHOLD));
+        } else {
+            assertArrayEquals(stats.mDns, makeTestDnsTimeoutNano(0 /* times */));
+        }
+    }
+
+    private DataStallDetectionStats makeTestDataStallDetectionStats(int evaluationType,
+            int transportType) {
+        final DataStallDetectionStats.Builder stats = new DataStallDetectionStats.Builder()
+                .setEvaluationType(evaluationType)
+                .setNetworkType(transportType);
+        switch (transportType) {
+            case NetworkCapabilities.TRANSPORT_CELLULAR:
+                stats.setCellData(TelephonyManager.NETWORK_TYPE_LTE /* radioType */,
                         true /* roaming */,
                         TEST_MCCMNC /* networkMccmnc */,
                         TEST_MCCMNC /* simMccmnc */,
                         CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN /* signalStrength */);
-        generateTimeoutDnsEvent(stats, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+                break;
+            case NetworkCapabilities.TRANSPORT_WIFI:
+                setupTestWifiInfo();
+                stats.setWiFiData(mWifiInfo);
+                break;
+            default:
+                break;
+        }
 
-        assertEquals(wrappedMonitor.buildDataStallDetectionStats(
-                 NetworkCapabilities.TRANSPORT_CELLULAR), stats.build());
+        if ((evaluationType & DATA_STALL_EVALUATION_TYPE_TCP) != 0) {
+            generateTestTcpStats(stats);
+        }
 
+        if ((evaluationType & DATA_STALL_EVALUATION_TYPE_DNS) != 0) {
+            generateTimeoutDnsEvent(stats, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+        }
+
+        return stats.build();
+    }
+
+    private byte[] makeTestDnsTimeoutNano(int timeoutCount) {
+        // Make a expected nano dns message.
+        final DnsEvent event = new DnsEvent();
+        event.dnsReturnCode = new int[timeoutCount];
+        event.dnsTime = new long[timeoutCount];
+        Arrays.fill(event.dnsReturnCode, RETURN_CODE_DNS_TIMEOUT);
+        Arrays.fill(event.dnsTime, TEST_ELAPSED_TIME_MS);
+        return MessageNano.toByteArray(event);
+    }
+
+    private byte[] makeTestCellDataNano() {
+        final CellularData data = new CellularData();
+        data.ratType = DataStallEventProto.RADIO_TECHNOLOGY_LTE;
+        data.networkMccmnc = TEST_MCCMNC;
+        data.simMccmnc = TEST_MCCMNC;
+        data.isRoaming = true;
+        data.signalStrength = 0;
+        return MessageNano.toByteArray(data);
+    }
+
+    private byte[] makeTestWifiDataNano() {
+        final WifiData data = new WifiData();
+        data.wifiBand = DataStallEventProto.AP_BAND_2GHZ;
+        data.signalStrength = TEST_SIGNAL_STRENGTH;
+        return MessageNano.toByteArray(data);
+    }
+
+    private void setupTestWifiInfo() {
         when(mWifi.getConnectionInfo()).thenReturn(mWifiInfo);
+        when(mWifiInfo.getRssi()).thenReturn(TEST_SIGNAL_STRENGTH);
+        // Set to 2.4G band. Map to DataStallEventProto.AP_BAND_2GHZ proto definition.
+        when(mWifiInfo.getFrequency()).thenReturn(2450);
+    }
 
-        stats = new DataStallDetectionStats.Builder()
-                .setEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS)
-                .setNetworkType(NetworkCapabilities.TRANSPORT_WIFI)
-                .setWiFiData(mWifiInfo);
-        generateTimeoutDnsEvent(stats, DEFAULT_DNS_TIMEOUT_THRESHOLD);
+    private void testDataStallMetricsWithCellular(int evalType) {
+        testDataStallMetrics(evalType, NetworkCapabilities.TRANSPORT_CELLULAR);
+    }
 
-        assertEquals(
-                wrappedMonitor.buildDataStallDetectionStats(NetworkCapabilities.TRANSPORT_WIFI),
-                stats.build());
+    private void testDataStallMetricsWithWiFi(int evalType) {
+        testDataStallMetrics(evalType, NetworkCapabilities.TRANSPORT_WIFI);
+    }
+
+    private void testDataStallMetrics(int evalType, int transportType) {
+        setDataStallEvaluationType(evalType);
+        final NetworkCapabilities nc = new NetworkCapabilities()
+                .addTransportType(transportType)
+                .addCapability(NET_CAPABILITY_INTERNET);
+        final WrappedNetworkMonitor wrappedMonitor = makeMonitor(nc);
+        setupTestWifiInfo();
+        final DataStallDetectionStats stats =
+                makeTestDataStallDetectionStats(evalType, transportType);
+        assertEquals(wrappedMonitor.buildDataStallDetectionStats(transportType, evalType), stats);
+
+        if ((evalType & DATA_STALL_EVALUATION_TYPE_TCP) != 0) {
+            verify(mTst, timeout(HANDLER_TIMEOUT_MS).atLeastOnce()).getLatestPacketFailPercentage();
+        } else {
+            verify(mTst, never()).getLatestPacketFailPercentage();
+        }
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_DnsWithCellular() {
+        testDataStallMetricsWithCellular(DATA_STALL_EVALUATION_TYPE_DNS);
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_DnsWithWiFi() {
+        testDataStallMetricsWithWiFi(DATA_STALL_EVALUATION_TYPE_DNS);
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_TcpWithCellular() {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDataStallMetricsWithCellular(DATA_STALL_EVALUATION_TYPE_TCP);
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_TcpWithWiFi() {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDataStallMetricsWithWiFi(DATA_STALL_EVALUATION_TYPE_TCP);
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_TcpAndDnsWithWifi() {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDataStallMetricsWithWiFi(
+                DATA_STALL_EVALUATION_TYPE_TCP | DATA_STALL_EVALUATION_TYPE_DNS);
+    }
+
+    @Test
+    public void testCollectDataStallMetrics_TcpAndDnsWithCellular() {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDataStallMetricsWithCellular(
+                DATA_STALL_EVALUATION_TYPE_TCP | DATA_STALL_EVALUATION_TYPE_DNS);
     }
 
     @Test
@@ -2334,10 +2561,16 @@
 
     private void generateTimeoutDnsEvent(DataStallDetectionStats.Builder stats, int num) {
         for (int i = 0; i < num; i++) {
-            stats.addDnsEvent(RETURN_CODE_DNS_TIMEOUT, 123456789 /* timeMs */);
+            stats.addDnsEvent(RETURN_CODE_DNS_TIMEOUT, TEST_ELAPSED_TIME_MS /* timeMs */);
         }
     }
 
+    private void generateTestTcpStats(DataStallDetectionStats.Builder stats) {
+        when(mTst.getLatestPacketFailPercentage()).thenReturn(TEST_TCP_FAIL_RATE);
+        when(mTst.getSentSinceLastRecv()).thenReturn(TEST_TCP_PACKET_COUNT);
+        stats.setTcpFailRate(TEST_TCP_FAIL_RATE).setTcpSentSinceLastRecv(TEST_TCP_PACKET_COUNT);
+    }
+
     private NetworkTestResultParcelable matchNetworkTestResultParcelable(final int result,
             final int probesSucceeded) {
         return matchNetworkTestResultParcelable(result, probesSucceeded, null /* redirectUrl */);