Merge "Data stall detection using DNS event"
am: 33fe80f927

Change-Id: I4b5248e97028bf8e9e837ff5ccf8e0d3138f7b05
diff --git a/core/java/android/net/metrics/NetworkEvent.java b/core/java/android/net/metrics/NetworkEvent.java
index 1999e78..cb82fbe 100644
--- a/core/java/android/net/metrics/NetworkEvent.java
+++ b/core/java/android/net/metrics/NetworkEvent.java
@@ -44,6 +44,8 @@
     public static final int NETWORK_FIRST_VALIDATION_PORTAL_FOUND = 10;
     public static final int NETWORK_REVALIDATION_PORTAL_FOUND     = 11;
 
+    public static final int NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND = 12;
+
     @IntDef(value = {
             NETWORK_CONNECTED,
             NETWORK_VALIDATED,
@@ -56,6 +58,7 @@
             NETWORK_REVALIDATION_SUCCESS,
             NETWORK_FIRST_VALIDATION_PORTAL_FOUND,
             NETWORK_REVALIDATION_PORTAL_FOUND,
+            NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventType {}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 258426e..258c9a9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10436,6 +10436,41 @@
         public static final String CAPTIVE_PORTAL_USER_AGENT = "captive_portal_user_agent";
 
         /**
+         * The threshold value for the number of consecutive dns timeout events received to be a
+         * signal of data stall. Set the value to 0 or less than 0 to disable. Note that the value
+         * should be larger than 0 if the DNS data stall detection is enabled.
+         *
+         * @hide
+         */
+        public static final String DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD =
+                "data_stall_consecutive_dns_timeout_threshold";
+
+        /**
+         * The minimal time interval in milliseconds for data stall reevaluation.
+         *
+         * @hide
+         */
+        public static final String DATA_STALL_MIN_EVALUATE_INTERVAL =
+                "data_stall_min_evaluate_interval";
+
+        /**
+         * DNS timeouts older than this timeout (in milliseconds) are not considered for detecting
+         * a data stall.
+         *
+         * @hide
+         */
+        public static final String DATA_STALL_VALID_DNS_TIME_THRESHOLD =
+                "data_stall_valid_dns_time_threshold";
+
+        /**
+         * Which data stall detection signal to use. Possible values are a union of the powers of 2
+         * of DATA_STALL_EVALUATION_TYPE_*.
+         *
+         * @hide
+         */
+        public static final String DATA_STALL_EVALUATION_TYPE = "data_stall_evaluation_type";
+
+        /**
          * Whether network service discovery is enabled.
          *
          * @hide
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 9d3085a..f2f28937 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -185,6 +185,10 @@
                     Settings.Global.DATA_ROAMING,
                     Settings.Global.DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS,
                     Settings.Global.DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS,
+                    Settings.Global.DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD,
+                    Settings.Global.DATA_STALL_EVALUATION_TYPE,
+                    Settings.Global.DATA_STALL_MIN_EVALUATE_INTERVAL,
+                    Settings.Global.DATA_STALL_VALID_DNS_TIME_THRESHOLD,
                     Settings.Global.DEBUG_APP,
                     Settings.Global.DEBUG_VIEW_ATTRIBUTES,
                     Settings.Global.DEFAULT_DNS_SERVER,
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 564d35a..8a0d9fe 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1667,6 +1667,24 @@
                 loge("Error parsing ip address in validation event");
             }
         }
+
+        @Override
+        public void onDnsEvent(int netId, int eventType, int returnCode, String hostname,
+                String[] ipAddresses, int ipAddressesCount, long timestamp, int uid) {
+            NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+            // Netd event only allow registrants from system. Each NetworkMonitor thread is under
+            // the caller thread of registerNetworkAgent. Thus, it's not allowed to register netd
+            // event callback for certain nai. e.g. cellular. Register here to pass to
+            // NetworkMonitor instead.
+            // TODO: Move the Dns Event to NetworkMonitor. Use Binder.clearCallingIdentity() in
+            // registerNetworkAgent to have NetworkMonitor created with system process as design
+            // expectation. Also, NetdEventListenerService only allow one callback from each
+            // caller type. Need to re-factor NetdEventListenerService to allow multiple
+            // NetworkMonitor registrants.
+            if (nai != null && nai.satisfies(mDefaultRequest)) {
+                nai.networkMonitor.sendMessage(NetworkMonitor.EVENT_DNS_NOTIFICATION, returnCode);
+            }
+        }
     };
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index c3e3842..c2f4406 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -72,6 +72,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Protocol;
+import com.android.internal.util.RingBufferIndices;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
@@ -99,7 +100,7 @@
     private static final String TAG = NetworkMonitor.class.getSimpleName();
     private static final boolean DBG  = true;
     private static final boolean VDBG = false;
-
+    private static final boolean VDBG_STALL = Log.isLoggable(TAG, Log.DEBUG);
     // Default configuration values for captive portal detection probes.
     // TODO: append a random length parameter to the default HTTPS url.
     // TODO: randomize browser version ids in the default User-Agent String.
@@ -116,6 +117,15 @@
     private static final int SOCKET_TIMEOUT_MS = 10000;
     private static final int PROBE_TIMEOUT_MS  = 3000;
 
+    // Default configuration values for data stall detection.
+    private static final int DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD = 5;
+    private static final int DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS = 60 * 1000;
+    private static final int DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS = 30 * 60 * 1000;
+
+    private static final int DATA_STALL_EVALUATION_TYPE_DNS = 1;
+    private static final int DEFAULT_DATA_STALL_EVALUATION_TYPES =
+            (1 << DATA_STALL_EVALUATION_TYPE_DNS);
+
     static enum EvaluationResult {
         VALIDATED(true),
         CAPTIVE_PORTAL(false);
@@ -233,6 +243,12 @@
      */
     public static final int CMD_PROBE_COMPLETE = BASE + 16;
 
+    /**
+     * ConnectivityService notifies NetworkMonitor of DNS query responses event.
+     * arg1 = returncode in OnDnsEvent which indicates the response code for the DNS query.
+     */
+    public static final int EVENT_DNS_NOTIFICATION = BASE + 17;
+
     // Start mReevaluateDelayMs at this value and double.
     private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
     private static final int MAX_REEVALUATE_DELAY_MS = 10*60*1000;
@@ -314,6 +330,12 @@
     private int mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
     private int mEvaluateAttempts = 0;
     private volatile int mProbeToken = 0;
+    private final int mConsecutiveDnsTimeoutThreshold;
+    private final int mDataStallMinEvaluateTime;
+    private final int mDataStallValidDnsTimeThreshold;
+    private final int mDataStallEvaluationType;
+    private final DnsStallDetector mDnsStallDetector;
+    private long mLastProbeTime;
 
     public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
             NetworkRequest defaultRequest) {
@@ -359,6 +381,12 @@
         mCaptivePortalFallbackUrls = makeCaptivePortalFallbackUrls();
         mCaptivePortalFallbackSpecs = makeCaptivePortalFallbackProbeSpecs();
         mRandom = deps.getRandom();
+        // TODO: Evaluate to move data stall configuration to a specific class.
+        mConsecutiveDnsTimeoutThreshold = getConsecutiveDnsTimeoutThreshold();
+        mDnsStallDetector = new DnsStallDetector(mConsecutiveDnsTimeoutThreshold);
+        mDataStallMinEvaluateTime = getDataStallMinEvaluateTime();
+        mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold();
+        mDataStallEvaluationType = getDataStallEvalutionType();
 
         start();
     }
@@ -507,6 +535,9 @@
                     sendMessage(CMD_EVALUATE_PRIVATE_DNS);
                     break;
                 }
+                case EVENT_DNS_NOTIFICATION:
+                    mDnsStallDetector.accumulateConsecutiveDnsTimeoutCount(message.arg1);
+                    break;
                 default:
                     break;
             }
@@ -537,6 +568,13 @@
                 case CMD_EVALUATE_PRIVATE_DNS:
                     transitionTo(mEvaluatingPrivateDnsState);
                     break;
+                case EVENT_DNS_NOTIFICATION:
+                    mDnsStallDetector.accumulateConsecutiveDnsTimeoutCount(message.arg1);
+                    if (isDataStall()) {
+                        validationLog("Suspecting data stall, reevaluate");
+                        transitionTo(mEvaluatingState);
+                    }
+                    break;
                 default:
                     return NOT_HANDLED;
             }
@@ -856,6 +894,7 @@
 
                     final CaptivePortalProbeResult probeResult =
                             (CaptivePortalProbeResult) message.obj;
+                    mLastProbeTime = SystemClock.elapsedRealtime();
                     if (probeResult.isSuccessful()) {
                         // Transit EvaluatingPrivateDnsState to get to Validated
                         // state (even if no Private DNS validation required).
@@ -883,6 +922,7 @@
                     // Leave the event to EvaluatingState. Defer this message will result in reset
                     // of mReevaluateDelayMs and mEvaluateAttempts.
                 case CMD_NETWORK_DISCONNECTED:
+                case EVENT_DNS_NOTIFICATION:
                     return NOT_HANDLED;
                 default:
                     // TODO: Some events may able to handle in this state, instead of deferring to
@@ -947,6 +987,29 @@
                 Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
     }
 
+    private int getConsecutiveDnsTimeoutThreshold() {
+        return mDependencies.getSetting(mContext,
+                Settings.Global.DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD,
+                DEFAULT_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD);
+    }
+
+    private int getDataStallMinEvaluateTime() {
+        return mDependencies.getSetting(mContext,
+                Settings.Global.DATA_STALL_MIN_EVALUATE_INTERVAL,
+                DEFAULT_DATA_STALL_MIN_EVALUATE_TIME_MS);
+    }
+
+    private int getDataStallValidDnsTimeThreshold() {
+        return mDependencies.getSetting(mContext,
+                Settings.Global.DATA_STALL_VALID_DNS_TIME_THRESHOLD,
+                DEFAULT_DATA_STALL_VALID_DNS_TIME_THRESHOLD_MS);
+    }
+
+    private int getDataStallEvalutionType() {
+        return mDependencies.getSetting(mContext, Settings.Global.DATA_STALL_EVALUATION_TYPE,
+                DEFAULT_DATA_STALL_EVALUATION_TYPES);
+    }
+
     // Static for direct access by ConnectivityService
     public static String getCaptivePortalServerHttpUrl(Context context) {
         return getCaptivePortalServerHttpUrl(Dependencies.DEFAULT, context);
@@ -1462,4 +1525,127 @@
 
         public static final Dependencies DEFAULT = new Dependencies();
     }
+
+    /**
+     * Methods in this class perform no locking because all accesses are performed on the state
+     * machine's thread. Need to consider the thread safety if it ever could be accessed outside the
+     * state machine.
+     */
+    @VisibleForTesting
+    protected class DnsStallDetector {
+        private static final int DEFAULT_DNS_LOG_SIZE = 50;
+        private int mConsecutiveTimeoutCount = 0;
+        private int mSize;
+        final DnsResult[] mDnsEvents;
+        final RingBufferIndices mResultIndices;
+
+        DnsStallDetector(int size) {
+            mSize = Math.max(DEFAULT_DNS_LOG_SIZE, size);
+            mDnsEvents = new DnsResult[mSize];
+            mResultIndices = new RingBufferIndices(mSize);
+        }
+
+        @VisibleForTesting
+        protected void accumulateConsecutiveDnsTimeoutCount(int code) {
+            final DnsResult result = new DnsResult(code);
+            mDnsEvents[mResultIndices.add()] = result;
+            if (result.isTimeout()) {
+                mConsecutiveTimeoutCount++;
+            } else {
+                // Keep the event in mDnsEvents without clearing it so that there are logs to do the
+                // simulation and analysis.
+                mConsecutiveTimeoutCount = 0;
+            }
+        }
+
+        private boolean isDataStallSuspected(int timeoutCountThreshold, int validTime) {
+            if (timeoutCountThreshold <= 0) {
+                Log.wtf(TAG, "Timeout count threshold should be larger than 0.");
+                return false;
+            }
+
+            // Check if the consecutive timeout count reach the threshold or not.
+            if (mConsecutiveTimeoutCount < timeoutCountThreshold) {
+                return false;
+            }
+
+            // Check if the target dns event index is valid or not.
+            final int firstConsecutiveTimeoutIndex =
+                    mResultIndices.indexOf(mResultIndices.size() - timeoutCountThreshold);
+
+            // If the dns timeout events happened long time ago, the events are meaningless for
+            // data stall evaluation. Thus, check if the first consecutive timeout dns event
+            // considered in the evaluation happened in defined threshold time.
+            final long now = SystemClock.elapsedRealtime();
+            final long firstTimeoutTime = now - mDnsEvents[firstConsecutiveTimeoutIndex].mTimeStamp;
+            return (firstTimeoutTime < validTime);
+        }
+
+        int getConsecutiveTimeoutCount() {
+            return mConsecutiveTimeoutCount;
+        }
+    }
+
+    private static class DnsResult {
+        // TODO: Need to move the DNS return code definition to a specific class once unify DNS
+        // response code is done.
+        private static final int RETURN_CODE_DNS_TIMEOUT = 255;
+
+        private final long mTimeStamp;
+        private final int mReturnCode;
+
+        DnsResult(int code) {
+            mTimeStamp = SystemClock.elapsedRealtime();
+            mReturnCode = code;
+        }
+
+        private boolean isTimeout() {
+            return mReturnCode == RETURN_CODE_DNS_TIMEOUT;
+        }
+    }
+
+
+    @VisibleForTesting
+    protected DnsStallDetector getDnsStallDetector() {
+        return mDnsStallDetector;
+    }
+
+    private boolean dataStallEvaluateTypeEnabled(int type) {
+        return (mDataStallEvaluationType & (1 << type)) != 0;
+    }
+
+    @VisibleForTesting
+    protected long getLastProbeTime() {
+        return mLastProbeTime;
+    }
+
+    @VisibleForTesting
+    protected boolean isDataStall() {
+        boolean result = false;
+        // Reevaluation will generate traffic. Thus, set a minimal reevaluation timer to limit the
+        // possible traffic cost in metered network.
+        if (mNetworkAgentInfo.networkCapabilities.isMetered()
+                && (SystemClock.elapsedRealtime() - getLastProbeTime()
+                < mDataStallMinEvaluateTime)) {
+            return false;
+        }
+
+        // Check dns signal. Suspect it may be a data stall if both :
+        // 1. The number of consecutive DNS query timeouts > mConsecutiveDnsTimeoutThreshold.
+        // 2. Those consecutive DNS queries happened in the last mValidDataStallDnsTimeThreshold ms.
+        if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_DNS)) {
+            if (mDnsStallDetector.isDataStallSuspected(mConsecutiveDnsTimeoutThreshold,
+                    mDataStallValidDnsTimeThreshold)) {
+                result = true;
+                logNetworkEvent(NetworkEvent.NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND);
+            }
+        }
+
+        if (VDBG_STALL) {
+            log("isDataStall: result=" + result + ", consecutive dns timeout count="
+                    + mDnsStallDetector.getConsecutiveTimeoutCount());
+        }
+
+        return result;
+    }
 }
diff --git a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java b/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
index b399b0d..6e07b26 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
@@ -40,6 +40,7 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.wifi.WifiManager;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.provider.Settings;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -70,6 +71,7 @@
     private @Mock Handler mHandler;
     private @Mock IpConnectivityLog mLogger;
     private @Mock NetworkAgentInfo mAgent;
+    private @Mock NetworkAgentInfo mNotMeteredAgent;
     private @Mock NetworkInfo mNetworkInfo;
     private @Mock NetworkRequest mRequest;
     private @Mock TelephonyManager mTelephony;
@@ -87,6 +89,10 @@
     private static final String TEST_FALLBACK_URL = "http://fallback.google.com/gen_204";
     private static final String TEST_OTHER_FALLBACK_URL = "http://otherfallback.google.com/gen_204";
 
+    private static final int DATA_STALL_EVALUATION_TYPE_DNS = 1;
+    private static final int RETURN_CODE_DNS_SUCCESS = 0;
+    private static final int RETURN_CODE_DNS_TIMEOUT = 255;
+
     @Before
     public void setUp() throws IOException {
         MockitoAnnotations.initMocks(this);
@@ -95,6 +101,12 @@
                 .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
         mAgent.networkInfo = mNetworkInfo;
 
+        mNotMeteredAgent.linkProperties = new LinkProperties();
+        mNotMeteredAgent.networkCapabilities = new NetworkCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        mNotMeteredAgent.networkInfo = mNetworkInfo;
+
         when(mAgent.network()).thenReturn(mNetwork);
         when(mDependencies.getNetwork(any())).thenReturn(mNetwork);
         when(mDependencies.getRandom()).thenReturn(mRandom);
@@ -138,6 +150,40 @@
         when(mNetwork.getAllByName(any())).thenReturn(new InetAddress[] {
             InetAddress.parseNumericAddress("192.168.0.0")
         });
+
+        setMinDataStallEvaluateInterval(500);
+        setDataStallEvaluationType(1 << DATA_STALL_EVALUATION_TYPE_DNS);
+        setValidDataStallDnsTimeThreshold(500);
+        setConsecutiveDnsTimeoutThreshold(5);
+    }
+
+    private class WrappedNetworkMonitor extends NetworkMonitor {
+        private long mProbeTime = 0;
+
+        WrappedNetworkMonitor(Context context, Handler handler,
+                NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest,
+                IpConnectivityLog logger, Dependencies deps) {
+                super(context, handler, networkAgentInfo, defaultRequest, logger, deps);
+        }
+
+        @Override
+        protected long getLastProbeTime() {
+            return mProbeTime;
+        }
+
+        protected void setLastProbeTime(long time) {
+            mProbeTime = time;
+        }
+    }
+
+    WrappedNetworkMonitor makeMeteredWrappedNetworkMonitor() {
+        return new WrappedNetworkMonitor(
+                mContext, mHandler, mAgent, mRequest, mLogger, mDependencies);
+    }
+
+    WrappedNetworkMonitor makeNotMeteredWrappedNetworkMonitor() {
+        return new WrappedNetworkMonitor(
+                mContext, mHandler, mNotMeteredAgent, mRequest, mLogger, mDependencies);
     }
 
     NetworkMonitor makeMonitor() {
@@ -272,6 +318,113 @@
         assertPortal(makeMonitor().isCaptivePortal());
     }
 
+    @Test
+    public void testIsDataStall_EvaluationDisabled() {
+        setDataStallEvaluationType(0);
+        WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
+        assertFalse(wrappedMonitor.isDataStall());
+    }
+
+    @Test
+    public void testIsDataStall_EvaluationDnsOnNotMeteredNetwork() {
+        WrappedNetworkMonitor wrappedMonitor = makeNotMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
+        makeDnsTimeoutEvent(wrappedMonitor, 5);
+        assertTrue(wrappedMonitor.isDataStall());
+    }
+
+    @Test
+    public void testIsDataStall_EvaluationDnsOnMeteredNetwork() {
+        WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
+        assertFalse(wrappedMonitor.isDataStall());
+
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        makeDnsTimeoutEvent(wrappedMonitor, 5);
+        assertTrue(wrappedMonitor.isDataStall());
+    }
+
+    @Test
+    public void testIsDataStall_EvaluationDnsWithDnsTimeoutCount() {
+        WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        makeDnsTimeoutEvent(wrappedMonitor, 3);
+        assertFalse(wrappedMonitor.isDataStall());
+        // Reset consecutive timeout counts.
+        makeDnsSuccessEvent(wrappedMonitor, 1);
+        makeDnsTimeoutEvent(wrappedMonitor, 2);
+        assertFalse(wrappedMonitor.isDataStall());
+
+        makeDnsTimeoutEvent(wrappedMonitor, 3);
+        assertTrue(wrappedMonitor.isDataStall());
+
+        // Set the value to larger than the default dns log size.
+        setConsecutiveDnsTimeoutThreshold(51);
+        wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        makeDnsTimeoutEvent(wrappedMonitor, 50);
+        assertFalse(wrappedMonitor.isDataStall());
+
+        makeDnsTimeoutEvent(wrappedMonitor, 1);
+        assertTrue(wrappedMonitor.isDataStall());
+    }
+
+    @Test
+    public void testIsDataStall_EvaluationDnsWithDnsTimeThreshold() {
+        // Test dns events happened in valid dns time threshold.
+        WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
+        makeDnsTimeoutEvent(wrappedMonitor, 5);
+        assertFalse(wrappedMonitor.isDataStall());
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        assertTrue(wrappedMonitor.isDataStall());
+
+        // Test dns events happened before valid dns time threshold.
+        setValidDataStallDnsTimeThreshold(0);
+        wrappedMonitor = makeMeteredWrappedNetworkMonitor();
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 100);
+        makeDnsTimeoutEvent(wrappedMonitor, 5);
+        assertFalse(wrappedMonitor.isDataStall());
+        wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000);
+        assertFalse(wrappedMonitor.isDataStall());
+    }
+
+    private void makeDnsTimeoutEvent(WrappedNetworkMonitor wrappedMonitor, int count) {
+        for (int i = 0; i < count; i++) {
+            wrappedMonitor.getDnsStallDetector().accumulateConsecutiveDnsTimeoutCount(
+                    RETURN_CODE_DNS_TIMEOUT);
+        }
+    }
+
+    private void makeDnsSuccessEvent(WrappedNetworkMonitor wrappedMonitor, int count) {
+        for (int i = 0; i < count; i++) {
+            wrappedMonitor.getDnsStallDetector().accumulateConsecutiveDnsTimeoutCount(
+                    RETURN_CODE_DNS_SUCCESS);
+        }
+    }
+
+    private void setDataStallEvaluationType(int type) {
+        when(mDependencies.getSetting(any(),
+            eq(Settings.Global.DATA_STALL_EVALUATION_TYPE), anyInt())).thenReturn(type);
+    }
+
+    private void setMinDataStallEvaluateInterval(int time) {
+        when(mDependencies.getSetting(any(),
+            eq(Settings.Global.DATA_STALL_MIN_EVALUATE_INTERVAL), anyInt())).thenReturn(time);
+    }
+
+    private void setValidDataStallDnsTimeThreshold(int time) {
+        when(mDependencies.getSetting(any(),
+            eq(Settings.Global.DATA_STALL_VALID_DNS_TIME_THRESHOLD), anyInt())).thenReturn(time);
+    }
+
+    private void setConsecutiveDnsTimeoutThreshold(int num) {
+        when(mDependencies.getSetting(any(),
+            eq(Settings.Global.DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD), anyInt()))
+            .thenReturn(num);
+    }
+
     private void setFallbackUrl(String url) {
         when(mDependencies.getSetting(any(),
                 eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL), any())).thenReturn(url);