Wifi usability: Data stall detection based on link layer stats

This is an effort to define new criterion for triggering Wifi data stall:
(1) Defined data stall criterion based on transmit and/or receive throughput, transmit packet error rate, and CCA level. Data stall is triggered when transmit and/or receive links are consecutively bad over multiple RSSI polls.
(2) Added DeviceConfig flags (that is disabled by default) which may be configured on the server side to tune the thresholds at which data stall gets triggered.

Bug: 141027476

Test: frameworks/opt/net/wifi/tests/wifitests/runtests.sh

Change-Id: I3c5d9756ff74c0b5682a0b2a51ed64c27c679d72
Signed-off-by: Mingguang Xu <mingguangxu@google.com>
Merged-In: I76d6338cd2d482d198fde1e5a2d1a0540c087ca6
diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java
index 8e8361e..b803712 100644
--- a/service/java/com/android/server/wifi/ClientModeImpl.java
+++ b/service/java/com/android/server/wifi/ClientModeImpl.java
@@ -765,6 +765,11 @@
     private final WrongPasswordNotifier mWrongPasswordNotifier;
     private WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager;
     private boolean mConnectedMacRandomzationSupported;
+    // Maximum duration to continue to log Wifi usability stats after a data stall is triggered.
+    @VisibleForTesting
+    public static final long DURATION_TO_WAIT_ADD_STATS_AFTER_DATA_STALL_MS = 30 * 1000;
+    private long mDataStallTriggerTimeMs = -1;
+    private int mLastStatusDataStall = WifiIsUnusableEvent.TYPE_UNKNOWN;
 
     public ClientModeImpl(Context context, FrameworkFacade facade, Looper looper,
                             UserManager userManager, WifiInjector wifiInjector,
@@ -5109,11 +5114,24 @@
                             }
                             mWifiScoreReport.noteIpCheck();
                         }
-                        int statusDataStall =
-                                mWifiDataStall.checkForDataStall(mLastLinkLayerStats, stats);
-                        if (statusDataStall != WifiIsUnusableEvent.TYPE_UNKNOWN) {
-                            mWifiMetrics.addToWifiUsabilityStatsList(WifiUsabilityStats.LABEL_BAD,
-                                    convertToUsabilityStatsTriggerType(statusDataStall), -1);
+                        int statusDataStall = mWifiDataStall.checkForDataStall(
+                                mLastLinkLayerStats, stats, mWifiInfo);
+                        if (mDataStallTriggerTimeMs == -1
+                                && statusDataStall != WifiIsUnusableEvent.TYPE_UNKNOWN) {
+                            mDataStallTriggerTimeMs = mClock.getElapsedSinceBootMillis();
+                            mLastStatusDataStall = statusDataStall;
+                        }
+                        if (mDataStallTriggerTimeMs != -1) {
+                            long elapsedTime =  mClock.getElapsedSinceBootMillis()
+                                    - mDataStallTriggerTimeMs;
+                            if (elapsedTime >= DURATION_TO_WAIT_ADD_STATS_AFTER_DATA_STALL_MS) {
+                                mDataStallTriggerTimeMs = -1;
+                                mWifiMetrics.addToWifiUsabilityStatsList(
+                                        WifiUsabilityStats.LABEL_BAD,
+                                        convertToUsabilityStatsTriggerType(mLastStatusDataStall),
+                                        -1);
+                                mLastStatusDataStall = WifiIsUnusableEvent.TYPE_UNKNOWN;
+                            }
                         }
                         mWifiMetrics.incrementWifiLinkLayerUsageStats(stats);
                         mLastLinkLayerStats = stats;
diff --git a/service/java/com/android/server/wifi/DeviceConfigFacade.java b/service/java/com/android/server/wifi/DeviceConfigFacade.java
index c64cd52..a9889f4 100644
--- a/service/java/com/android/server/wifi/DeviceConfigFacade.java
+++ b/service/java/com/android/server/wifi/DeviceConfigFacade.java
@@ -28,6 +28,17 @@
     private static final int DEFAULT_ABNORMAL_CONNECTION_DURATION_MS =
             (int) TimeUnit.SECONDS.toMillis(30);
     private static final String NAMESPACE = "wifi";
+    // Default duration for evaluating Wifi condition to trigger a data stall
+    // measured in milliseconds
+    public static final int DEFAULT_DATA_STALL_DURATION_MS = 1500;
+    // Default threshold of Tx throughput below which to trigger a data stall measured in Mbps
+    public static final int DEFAULT_DATA_STALL_TX_TPUT_THR_MBPS = 2;
+    // Default threshold of Rx throughput below which to trigger a data stall measured in Mbps
+    public static final int DEFAULT_DATA_STALL_RX_TPUT_THR_MBPS = 2;
+    // Default threshold of Tx packet error rate above which to trigger a data stall in percentage
+    public static final int DEFAULT_DATA_STALL_TX_PER_THR = 90;
+    // Default threshold of CCA level above which to trigger a data stall in percentage
+    public static final int DEFAULT_DATA_STALL_CCA_LEVEL_THR = 100;
 
     /**
      * Gets the feature flag for reporting abnormally long connections.
@@ -54,4 +65,44 @@
         DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, executor,
                 onPropertiesChangedListener);
     }
+
+    /**
+     * Gets the duration of evaluating Wifi condition to trigger a data stall.
+     */
+    public int getDataStallDurationMs() {
+        return DeviceConfig.getInt(NAMESPACE, "data_stall_duration_ms",
+                DEFAULT_DATA_STALL_DURATION_MS);
+    }
+
+    /**
+     * Gets the threshold of Tx throughput below which to trigger a data stall.
+     */
+    public int getDataStallTxTputThrMbps() {
+        return DeviceConfig.getInt(NAMESPACE, "data_stall_tx_tput_thr_mbps",
+                DEFAULT_DATA_STALL_TX_TPUT_THR_MBPS);
+    }
+
+    /**
+     * Gets the threshold of Rx throughput below which to trigger a data stall.
+     */
+    public int getDataStallRxTputThrMbps() {
+        return DeviceConfig.getInt(NAMESPACE, "data_stall_rx_tput_thr_mbps",
+                DEFAULT_DATA_STALL_RX_TPUT_THR_MBPS);
+    }
+
+    /**
+     * Gets the threshold of Tx packet error rate above which to trigger a data stall.
+     */
+    public int getDataStallTxPerThr() {
+        return DeviceConfig.getInt(NAMESPACE, "data_stall_tx_per_thr",
+                DEFAULT_DATA_STALL_TX_PER_THR);
+    }
+
+    /**
+     * Gets the threshold of CCA level above which to trigger a data stall.
+     */
+    public int getDataStallCcaLevelThr() {
+        return DeviceConfig.getInt(NAMESPACE, "data_stall_cca_level_thr",
+                DEFAULT_DATA_STALL_CCA_LEVEL_THR);
+    }
 }
diff --git a/service/java/com/android/server/wifi/WifiDataStall.java b/service/java/com/android/server/wifi/WifiDataStall.java
index 1054c2e..6eb3b41 100644
--- a/service/java/com/android/server/wifi/WifiDataStall.java
+++ b/service/java/com/android/server/wifi/WifiDataStall.java
@@ -17,6 +17,9 @@
 package com.android.server.wifi;
 
 import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.os.Handler;
+import android.os.Looper;
 import android.provider.Settings;
 
 import com.android.server.wifi.nano.WifiMetricsProto.WifiIsUnusableEvent;
@@ -33,19 +36,50 @@
     public static final int MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT = 50;
     // Maximum time gap between two WifiLinkLayerStats to trigger a data stall
     public static final long MAX_MS_DELTA_FOR_DATA_STALL = 60 * 1000; // 1 minute
+    // Maximum time that a data stall start time stays valid.
+    public static final long VALIDITY_PERIOD_OF_DATA_STALL_START_MS = 30 * 1000; // 0.5 minutes
+    // Default Tx packet error rate when there is no Tx attempt
+    public static final int DEFAULT_TX_PACKET_ERROR_RATE = 20;
+    // Default CCA level when CCA stats are not available
+    public static final int DEFAULT_CCA_LEVEL = 0;
 
     private final Context mContext;
+    private final DeviceConfigFacade mDeviceConfigFacade;
     private final FrameworkFacade mFacade;
     private final WifiMetrics mWifiMetrics;
 
+    private Handler mHandler;
     private int mMinTxBad;
     private int mMinTxSuccessWithoutRx;
+    private int mDataStallDurationMs;
+    private int mDataStallTxTputThrMbps;
+    private int mDataStallRxTputThrMbps;
+    private int mDataStallTxPerThr;
+    private int mDataStallCcaLevelThr;
+    private int mLastFrequency = -1;
+    private String mLastBssid;
+    private long mLastTotalRadioOnFreqTimeMs = -1;
+    private long mLastTotalCcaBusyFreqTimeMs = -1;
+    private long mDataStallStartTimeMs = -1;
+    private Clock mClock;
+    private boolean mDataStallTx = false;
+    private boolean mDataStallRx = false;
 
-    public WifiDataStall(Context context, FrameworkFacade facade, WifiMetrics wifiMetrics) {
+    public WifiDataStall(Context context, FrameworkFacade facade, WifiMetrics wifiMetrics,
+            DeviceConfigFacade deviceConfigFacade, Looper clientModeImplLooper, Clock clock) {
         mContext = context;
+        mDeviceConfigFacade = deviceConfigFacade;
         mFacade = facade;
+        mHandler = new Handler(clientModeImplLooper);
         mWifiMetrics = wifiMetrics;
+        mClock = clock;
         loadSettings();
+
+        mDeviceConfigFacade.addOnPropertiesChangedListener(
+                command -> mHandler.post(command),
+                properties -> {
+                    updateUsabilityDataCollectionFlags();
+                });
     }
 
     /**
@@ -59,15 +93,18 @@
                 MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT);
         mWifiMetrics.setWifiDataStallMinTxBad(mMinTxBad);
         mWifiMetrics.setWifiDataStallMinRxWithoutTx(mMinTxSuccessWithoutRx);
+        updateUsabilityDataCollectionFlags();
     }
 
     /**
      * Checks for data stall by looking at tx/rx packet counts
      * @param oldStats second most recent WifiLinkLayerStats
      * @param newStats most recent WifiLinkLayerStats
+     * @param wifiInfo WifiInfo for current connection
      * @return trigger type of WifiIsUnusableEvent
      */
-    public int checkForDataStall(WifiLinkLayerStats oldStats, WifiLinkLayerStats newStats) {
+    public int checkForDataStall(WifiLinkLayerStats oldStats, WifiLinkLayerStats newStats,
+            WifiInfo wifiInfo) {
         if (oldStats == null || newStats == null) {
             mWifiMetrics.resetWifiIsUnusableLinkLayerStats();
             return WifiIsUnusableEvent.TYPE_UNKNOWN;
@@ -103,25 +140,130 @@
 
         mWifiMetrics.updateWifiIsUnusableLinkLayerStats(txSuccessDelta, txRetriesDelta,
                 txBadDelta, rxSuccessDelta, timeMsDelta);
+
         if (timeMsDelta < MAX_MS_DELTA_FOR_DATA_STALL) {
-            // There is a data stall if there are too many tx failures
-            // or if we are not receiving any packets despite many tx successes
-            boolean dataStallBadTx = (txBadDelta >= mMinTxBad);
-            boolean dataStallTxSuccessWithoutRx =
-                    (rxSuccessDelta == 0 && txSuccessDelta >= mMinTxSuccessWithoutRx);
-            if (dataStallBadTx && dataStallTxSuccessWithoutRx) {
-                mWifiMetrics.logWifiIsUnusableEvent(WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH);
-                return WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH;
-            } else if (dataStallBadTx) {
-                mWifiMetrics.logWifiIsUnusableEvent(WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
-                return WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX;
-            } else if (dataStallTxSuccessWithoutRx) {
-                mWifiMetrics.logWifiIsUnusableEvent(
-                        WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX);
-                return WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX;
+            int txLinkSpeed = wifiInfo.getLinkSpeed();
+            int rxLinkSpeed = wifiInfo.getRxLinkSpeedMbps();
+            boolean isSameBssidAndFreq = mLastBssid == null || mLastFrequency == -1
+                    || (mLastBssid.equals(wifiInfo.getBSSID())
+                    && mLastFrequency == wifiInfo.getFrequency());
+            mLastFrequency = wifiInfo.getFrequency();
+            mLastBssid = wifiInfo.getBSSID();
+
+            int ccaLevel = updateCcaLevel(newStats, wifiInfo, isSameBssidAndFreq);
+            int txPer = updateTxPer(txSuccessDelta, txRetriesDelta, isSameBssidAndFreq);
+
+            boolean isTxTputLow = false;
+            boolean isRxTputLow = false;
+            if (txLinkSpeed > 0) {
+                int txTput = txLinkSpeed * (100 - txPer) * (100 - ccaLevel);
+                isTxTputLow = txTput < mDataStallTxTputThrMbps * 100 * 100;
+            }
+            if (rxLinkSpeed > 0) {
+                int rxTput = rxLinkSpeed * (100 - ccaLevel);
+                isRxTputLow = rxTput < mDataStallRxTputThrMbps * 100;
+            }
+
+            boolean dataStallTx = isTxTputLow || ccaLevel >= mDataStallCcaLevelThr
+                    || txPer >= mDataStallTxPerThr;
+            boolean dataStallRx = isRxTputLow || ccaLevel >= mDataStallCcaLevelThr;
+
+            // Data stall event is triggered if there are consecutive Tx and/or Rx data stalls
+            // Reset mDataStallStartTimeMs to -1 if currently there is no Tx or Rx data stall
+            if (dataStallTx || dataStallRx) {
+                mDataStallTx = mDataStallTx || dataStallTx;
+                mDataStallRx = mDataStallRx || dataStallRx;
+                if (mDataStallStartTimeMs == -1) {
+                    mDataStallStartTimeMs = mClock.getElapsedSinceBootMillis();
+                    if (mDataStallDurationMs == 0) {
+                        mDataStallStartTimeMs = -1;
+                        int result = calculateUsabilityEventType(mDataStallTx, mDataStallRx);
+                        mDataStallRx = false;
+                        mDataStallTx = false;
+                        return result;
+                    }
+                } else {
+                    long elapsedTime =  mClock.getElapsedSinceBootMillis() - mDataStallStartTimeMs;
+                    if (elapsedTime >= mDataStallDurationMs) {
+                        mDataStallStartTimeMs = -1;
+                        if (elapsedTime <= VALIDITY_PERIOD_OF_DATA_STALL_START_MS) {
+                            int result = calculateUsabilityEventType(mDataStallTx, mDataStallRx);
+                            mDataStallRx = false;
+                            mDataStallTx = false;
+                            return result;
+                        } else {
+                            mDataStallTx = false;
+                            mDataStallRx = false;
+                        }
+                    } else {
+                        // No need to do anything.
+                    }
+                }
+            } else {
+                mDataStallStartTimeMs = -1;
+                mDataStallTx = false;
+                mDataStallRx = false;
             }
         }
 
         return WifiIsUnusableEvent.TYPE_UNKNOWN;
     }
+
+    private int updateCcaLevel(WifiLinkLayerStats newStats, WifiInfo wifiInfo,
+            boolean isSameBssidAndFreq) {
+        WifiLinkLayerStats.ChannelStats statsMap = newStats.channelStatsMap.get(mLastFrequency);
+        if (statsMap == null || !isSameBssidAndFreq) {
+            return DEFAULT_CCA_LEVEL;
+        }
+        int radioOnTimeDelta = (int) (statsMap.radioOnTimeMs - mLastTotalRadioOnFreqTimeMs);
+        int ccaBusyTimeDelta = (int) (statsMap.ccaBusyTimeMs - mLastTotalCcaBusyFreqTimeMs);
+        mLastTotalRadioOnFreqTimeMs = statsMap.radioOnTimeMs;
+        mLastTotalCcaBusyFreqTimeMs = statsMap.ccaBusyTimeMs;
+
+        boolean isCcaValid = (radioOnTimeDelta > 0) && (ccaBusyTimeDelta >= 0)
+                && (ccaBusyTimeDelta <= radioOnTimeDelta);
+        // Update CCA level only if CCA stats are valid.
+        if (!isCcaValid) {
+            return DEFAULT_CCA_LEVEL;
+        }
+        return (int) (ccaBusyTimeDelta * 100 / radioOnTimeDelta);
+    }
+
+    private int updateTxPer(long txSuccessDelta, long txRetriesDelta, boolean isSameBssidAndFreq) {
+        if (!isSameBssidAndFreq) {
+            return DEFAULT_TX_PACKET_ERROR_RATE;
+        }
+        long txAttempts = txSuccessDelta + txRetriesDelta;
+        if (txAttempts <= 0) {
+            return DEFAULT_TX_PACKET_ERROR_RATE;
+        }
+        return (int) (txRetriesDelta * 100 / txAttempts);
+    }
+
+    private int calculateUsabilityEventType(boolean dataStallTx, boolean dataStallRx) {
+        int result = WifiIsUnusableEvent.TYPE_UNKNOWN;
+        if (dataStallTx && dataStallRx) {
+            result = WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH;
+        } else if (dataStallTx) {
+            result = WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX;
+        } else if (dataStallRx) {
+            result = WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX;
+        }
+        mWifiMetrics.logWifiIsUnusableEvent(result);
+        return result;
+    }
+
+    private void updateUsabilityDataCollectionFlags() {
+        mDataStallDurationMs = mDeviceConfigFacade.getDataStallDurationMs();
+        mDataStallTxTputThrMbps = mDeviceConfigFacade.getDataStallTxTputThrMbps();
+        mDataStallRxTputThrMbps = mDeviceConfigFacade.getDataStallRxTputThrMbps();
+        mDataStallTxPerThr = mDeviceConfigFacade.getDataStallTxPerThr();
+        mDataStallCcaLevelThr = mDeviceConfigFacade.getDataStallCcaLevelThr();
+
+        mWifiMetrics.setDataStallDurationMs(mDataStallDurationMs);
+        mWifiMetrics.setDataStallTxTputThrMbps(mDataStallTxTputThrMbps);
+        mWifiMetrics.setDataStallRxTputThrMbps(mDataStallRxTputThrMbps);
+        mWifiMetrics.setDataStallTxPerThr(mDataStallTxPerThr);
+        mWifiMetrics.setDataStallCcaLevelThr(mDataStallCcaLevelThr);
+    }
 }
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index 3a79e59..fe9ebea 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -301,7 +301,8 @@
         mWifiDiagnostics = new WifiDiagnostics(
                 mContext, this, mWifiNative, mBuildProperties,
                 new LastMileLogger(this), mClock);
-        mWifiDataStall = new WifiDataStall(mContext, mFrameworkFacade, mWifiMetrics);
+        mWifiDataStall = new WifiDataStall(mContext, mFrameworkFacade, mWifiMetrics,
+                mDeviceConfigFacade, clientModeImplLooper, mClock);
         mWifiMetrics.setWifiDataStall(mWifiDataStall);
         mLinkProbeManager = new LinkProbeManager(mClock, mWifiNative, mWifiMetrics,
                 mFrameworkFacade, mWifiCoreHandlerThread.getLooper(), mContext);
diff --git a/service/java/com/android/server/wifi/WifiMetrics.java b/service/java/com/android/server/wifi/WifiMetrics.java
index c0b04d3..1b7e8cd 100644
--- a/service/java/com/android/server/wifi/WifiMetrics.java
+++ b/service/java/com/android/server/wifi/WifiMetrics.java
@@ -2782,6 +2782,16 @@
                         + mExperimentValues.wifiDataStallMinTxSuccessWithoutRx);
                 pw.println("mExperimentValues.linkSpeedCountsLoggingEnabled="
                         + mExperimentValues.linkSpeedCountsLoggingEnabled);
+                pw.println("mExperimentValues.dataStallDurationMs="
+                        + mExperimentValues.dataStallDurationMs);
+                pw.println("mExperimentValues.dataStallTxTputThrMbps="
+                        + mExperimentValues.dataStallTxTputThrMbps);
+                pw.println("mExperimentValues.dataStallRxTputThrMbps="
+                        + mExperimentValues.dataStallRxTputThrMbps);
+                pw.println("mExperimentValues.dataStallTxPerThr="
+                        + mExperimentValues.dataStallTxPerThr);
+                pw.println("mExperimentValues.dataStallCcaLevelThr="
+                        + mExperimentValues.dataStallCcaLevelThr);
                 pw.println("WifiIsUnusableEventList: ");
                 for (WifiIsUnusableWithTime event : mWifiIsUnusableList) {
                     pw.println(event);
@@ -5191,4 +5201,49 @@
             mNumProvisionSuccess++;
         }
     }
+
+    /**
+     * Sets the duration for evaluating Wifi condition to trigger a data stall
+     */
+    public void setDataStallDurationMs(int duration) {
+        synchronized (mLock) {
+            mExperimentValues.dataStallDurationMs = duration;
+        }
+    }
+
+    /**
+     * Sets the threshold of Tx throughput below which to trigger a data stall
+     */
+    public void setDataStallTxTputThrMbps(int txTputThr) {
+        synchronized (mLock) {
+            mExperimentValues.dataStallTxTputThrMbps = txTputThr;
+        }
+    }
+
+    /**
+     * Sets the threshold of Rx throughput below which to trigger a data stall
+     */
+    public void setDataStallRxTputThrMbps(int rxTputThr) {
+        synchronized (mLock) {
+            mExperimentValues.dataStallRxTputThrMbps = rxTputThr;
+        }
+    }
+
+    /**
+     * Sets the threshold of Tx packet error rate above which to trigger a data stall
+     */
+    public void setDataStallTxPerThr(int txPerThr) {
+        synchronized (mLock) {
+            mExperimentValues.dataStallTxPerThr = txPerThr;
+        }
+    }
+
+    /**
+     * Sets the threshold of CCA level above which to trigger a data stall
+     */
+    public void setDataStallCcaLevelThr(int ccaLevel) {
+        synchronized (mLock) {
+            mExperimentValues.dataStallCcaLevelThr = ccaLevel;
+        }
+    }
 }
diff --git a/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java b/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
index a26582c..7896a8f 100644
--- a/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
@@ -3340,7 +3340,7 @@
         when(mWifiNative.getWifiLinkLayerStats(any())).thenReturn(newLLStats);
         mCmi.sendMessage(ClientModeImpl.CMD_RSSI_POLL, 1);
         mLooper.dispatchAll();
-        verify(mWifiDataStall).checkForDataStall(oldLLStats, newLLStats);
+        verify(mWifiDataStall).checkForDataStall(oldLLStats, newLLStats, mCmi.getWifiInfo());
         verify(mWifiMetrics).incrementWifiLinkLayerUsageStats(newLLStats);
     }
 
@@ -3356,7 +3356,7 @@
 
         WifiLinkLayerStats stats = new WifiLinkLayerStats();
         when(mWifiNative.getWifiLinkLayerStats(any())).thenReturn(stats);
-        when(mWifiDataStall.checkForDataStall(any(), any()))
+        when(mWifiDataStall.checkForDataStall(any(), any(), any()))
                 .thenReturn(WifiIsUnusableEvent.TYPE_UNKNOWN);
         mCmi.sendMessage(ClientModeImpl.CMD_RSSI_POLL, 1);
         mLooper.dispatchAll();
@@ -3364,11 +3364,16 @@
         verify(mWifiMetrics, never()).addToWifiUsabilityStatsList(WifiUsabilityStats.LABEL_BAD,
                 eq(anyInt()), eq(-1));
 
-        when(mWifiDataStall.checkForDataStall(any(), any()))
+        when(mWifiDataStall.checkForDataStall(any(), any(), any()))
                 .thenReturn(WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
         mCmi.sendMessage(ClientModeImpl.CMD_RSSI_POLL, 1);
         mLooper.dispatchAll();
         verify(mWifiMetrics, times(2)).updateWifiUsabilityStatsEntries(any(), eq(stats));
+        when(mClock.getElapsedSinceBootMillis())
+                .thenReturn(10L + ClientModeImpl.DURATION_TO_WAIT_ADD_STATS_AFTER_DATA_STALL_MS);
+        mCmi.sendMessage(ClientModeImpl.CMD_RSSI_POLL, 1);
+        mLooper.dispatchAll();
         verify(mWifiMetrics).addToWifiUsabilityStatsList(WifiUsabilityStats.LABEL_BAD,
                 WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX, -1);
     }
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiDataStallTest.java b/tests/wifitests/src/com/android/server/wifi/WifiDataStallTest.java
index a5f6852..71b6589 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiDataStallTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiDataStallTest.java
@@ -17,14 +17,17 @@
 package com.android.server.wifi;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyLong;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.os.Looper;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.Settings;
 
 import androidx.test.filters.SmallTest;
@@ -33,6 +36,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -46,9 +50,15 @@
     @Mock FrameworkFacade mFacade;
     @Mock WifiMetrics mWifiMetrics;
     WifiDataStall mWifiDataStall;
+    @Mock Clock mClock;
+    @Mock DeviceConfigFacade mDeviceConfigFacade;
+    @Mock Looper mClientModeImplLooper;
+    @Mock WifiInfo mWifiInfo;
 
     private final WifiLinkLayerStats mOldLlStats = new WifiLinkLayerStats();
     private final WifiLinkLayerStats mNewLlStats = new WifiLinkLayerStats();
+    final ArgumentCaptor<OnPropertiesChangedListener> mOnPropertiesChangedListenerCaptor =
+            ArgumentCaptor.forClass(OnPropertiesChangedListener.class);
 
     /**
      * Sets up for unit test
@@ -64,21 +74,38 @@
                 Settings.Global.WIFI_DATA_STALL_MIN_TX_SUCCESS_WITHOUT_RX,
                 WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT))
                 .thenReturn(WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT);
+        when(mDeviceConfigFacade.getDataStallDurationMs()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        when(mDeviceConfigFacade.getDataStallTxTputThrMbps()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_TX_TPUT_THR_MBPS);
+        when(mDeviceConfigFacade.getDataStallRxTputThrMbps()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_RX_TPUT_THR_MBPS);
+        when(mDeviceConfigFacade.getDataStallTxPerThr()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_TX_PER_THR);
+        when(mDeviceConfigFacade.getDataStallCcaLevelThr()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_CCA_LEVEL_THR);
+        when(mWifiInfo.getLinkSpeed()).thenReturn(100);
+        when(mWifiInfo.getRxLinkSpeedMbps()).thenReturn(100);
+        when(mWifiInfo.getFrequency()).thenReturn(5850);
+        when(mWifiInfo.getBSSID()).thenReturn("5G_WiFi");
 
-        mWifiDataStall = new WifiDataStall(mContext, mFacade, mWifiMetrics);
+        mWifiDataStall = new WifiDataStall(mContext, mFacade, mWifiMetrics, mDeviceConfigFacade,
+                mClientModeImplLooper, mClock);
 
         mOldLlStats.txmpdu_be = 1000;
-        mOldLlStats.retries_be = 2000;
+        mOldLlStats.retries_be = 1000;
         mOldLlStats.lostmpdu_be = 3000;
         mOldLlStats.rxmpdu_be = 4000;
         mOldLlStats.timeStampInMs = 10000;
 
-        mNewLlStats.txmpdu_be = mOldLlStats.txmpdu_be;
-        mNewLlStats.retries_be = mOldLlStats.retries_be;
+        mNewLlStats.txmpdu_be = 2 * mOldLlStats.txmpdu_be;
+        mNewLlStats.retries_be = 10 * mOldLlStats.retries_be;
         mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be;
         mNewLlStats.rxmpdu_be = mOldLlStats.rxmpdu_be;
         mNewLlStats.timeStampInMs = mOldLlStats.timeStampInMs
                 + WifiDataStall.MAX_MS_DELTA_FOR_DATA_STALL - 1;
+        verify(mDeviceConfigFacade).addOnPropertiesChangedListener(any(),
+                mOnPropertiesChangedListenerCaptor.capture());
     }
 
     /**
@@ -98,23 +125,53 @@
      */
     @Test
     public void verifyDataStallTxFailure() throws Exception {
-        mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be + WifiDataStall.MIN_TX_BAD_DEFAULT;
-        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verify(mWifiMetrics).logWifiIsUnusableEvent(WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
     }
 
     /**
+     * Verify there is no data stall from tx failures if tx failures are not consecutively bad
+     */
+    @Test
+    public void verifyNoDataStallWhenTxFailureIsNotConsecutive() throws Exception {
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
+        verifyUpdateWifiIsUnusableLinkLayerStats();
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        mNewLlStats.retries_be = 2 * mOldLlStats.retries_be;
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
+        verify(mWifiMetrics, never()).logWifiIsUnusableEvent(
+                WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
+    }
+
+    /**
      * Verify there is data stall from rx failures
      */
     @Test
     public void verifyDataStallRxFailure() throws Exception {
-        mNewLlStats.txmpdu_be =
-                mOldLlStats.txmpdu_be + WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT;
-        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+        when(mWifiInfo.getRxLinkSpeedMbps()).thenReturn(1);
+        mNewLlStats.retries_be = 2 * mOldLlStats.retries_be;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verify(mWifiMetrics).logWifiIsUnusableEvent(
                 WifiIsUnusableEvent.TYPE_DATA_STALL_TX_WITHOUT_RX);
     }
@@ -124,58 +181,61 @@
      */
     @Test
     public void verifyDataStallBothTxRxFailure() throws Exception {
-        mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be + WifiDataStall.MIN_TX_BAD_DEFAULT;
-        mNewLlStats.txmpdu_be =
-                mOldLlStats.txmpdu_be + WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT;
-        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+        when(mWifiInfo.getRxLinkSpeedMbps()).thenReturn(1);
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        assertEquals(WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verify(mWifiMetrics).logWifiIsUnusableEvent(WifiIsUnusableEvent.TYPE_DATA_STALL_BOTH);
     }
 
     /**
-     * Verify that we can change the minimum number of tx failures
-     * to trigger data stall from settings
+     * Verify that we can change the duration of evaluating Wifi conditions
+     * to trigger data stall from DeviceConfigFacade
      */
     @Test
-    public void verifyDataStallTxFailureSettingsChange() throws Exception {
-        when(mFacade.getIntegerSetting(mContext,
-                Settings.Global.WIFI_DATA_STALL_MIN_TX_BAD,
-                WifiDataStall.MIN_TX_BAD_DEFAULT))
-                .thenReturn(WifiDataStall.MIN_TX_BAD_DEFAULT + 1);
-        mWifiDataStall.loadSettings();
-        verify(mWifiMetrics).setWifiDataStallMinTxBad(WifiDataStall.MIN_TX_BAD_DEFAULT + 1);
-        verify(mWifiMetrics, times(2)).setWifiDataStallMinRxWithoutTx(
-                WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT);
+    public void verifyDataStallDurationDeviceConfigChange() throws Exception {
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+        when(mDeviceConfigFacade.getDataStallDurationMs()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS + 1);
+        mOnPropertiesChangedListenerCaptor.getValue().onPropertiesChanged(null);
 
-        mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be + WifiDataStall.MIN_TX_BAD_DEFAULT;
         assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
-        verify(mWifiMetrics, never()).logWifiIsUnusableEvent(anyInt());
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
+        verify(mWifiMetrics, never()).logWifiIsUnusableEvent(
+                WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
     }
 
     /**
-     * Verify that we can change the minimum number of tx successes when rx success is 0
-     * to trigger data stall from settings
+     * Verify that we can change the threshold of Tx packet error rate to trigger a data stall
+     * from DeviceConfigFacade
      */
     @Test
-    public void verifyDataStallRxFailureSettingsChange() throws Exception {
-        when(mFacade.getIntegerSetting(mContext,
-                Settings.Global.WIFI_DATA_STALL_MIN_TX_SUCCESS_WITHOUT_RX,
-                WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT))
-                .thenReturn(WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT + 1);
-        mWifiDataStall.loadSettings();
-        verify(mWifiMetrics, times(2)).setWifiDataStallMinTxBad(WifiDataStall.MIN_TX_BAD_DEFAULT);
-        verify(mWifiMetrics).setWifiDataStallMinRxWithoutTx(
-                WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT + 1);
+    public void verifyDataStallTxPerThrDeviceConfigChange() throws Exception {
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(10L);
+        when(mDeviceConfigFacade.getDataStallTxPerThr()).thenReturn(
+                DeviceConfigFacade.DEFAULT_DATA_STALL_TX_PER_THR + 1);
+        mOnPropertiesChangedListenerCaptor.getValue().onPropertiesChanged(null);
 
-        mNewLlStats.txmpdu_be =
-                mOldLlStats.txmpdu_be + WifiDataStall.MIN_TX_SUCCESS_WITHOUT_RX_DEFAULT;
         assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
-        verify(mWifiMetrics, never()).logWifiIsUnusableEvent(anyInt());
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(
+                10L + DeviceConfigFacade.DEFAULT_DATA_STALL_DURATION_MS);
+        assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
+        verify(mWifiMetrics, never()).logWifiIsUnusableEvent(
+                WifiIsUnusableEvent.TYPE_DATA_STALL_BAD_TX);
     }
 
     /**
@@ -184,7 +244,7 @@
     @Test
     public void verifyNoDataStallWhenNoFail() throws Exception {
         assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verify(mWifiMetrics, never()).resetWifiIsUnusableLinkLayerStats();
         verifyUpdateWifiIsUnusableLinkLayerStats();
         verify(mWifiMetrics, never()).logWifiIsUnusableEvent(anyInt());
@@ -196,11 +256,10 @@
      */
     @Test
     public void verifyNoDataStallBigTimeGap() throws Exception {
-        mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be + WifiDataStall.MIN_TX_BAD_DEFAULT;
         mNewLlStats.timeStampInMs = mOldLlStats.timeStampInMs
                 + WifiDataStall.MAX_MS_DELTA_FOR_DATA_STALL + 1;
         assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verifyUpdateWifiIsUnusableLinkLayerStats();
         verify(mWifiMetrics, never()).logWifiIsUnusableEvent(anyInt());
     }
@@ -212,7 +271,7 @@
     public void verifyReset() throws Exception {
         mNewLlStats.lostmpdu_be = mOldLlStats.lostmpdu_be - 1;
         assertEquals(WifiIsUnusableEvent.TYPE_UNKNOWN,
-                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats));
+                mWifiDataStall.checkForDataStall(mOldLlStats, mNewLlStats, mWifiInfo));
         verify(mWifiMetrics).resetWifiIsUnusableLinkLayerStats();
         verify(mWifiMetrics, never()).updateWifiIsUnusableLinkLayerStats(
                 anyLong(), anyLong(), anyLong(), anyLong(), anyLong());