diff options
author | Lucas Lin <lucaslin@google.com> | 2020-05-05 08:15:19 +0000 |
---|---|---|
committer | Lucas Lin <lucaslin@google.com> | 2020-05-06 08:39:13 +0000 |
commit | fc5814c128a03e495b62731b2801f7b661026dce (patch) | |
tree | 5e5a4f0f1345f04c297bb7d471be86abef819f2a | |
parent | aa2d8a3af4df361fe60560b1a50990d37dbfa817 (diff) |
Add EvaluatingBandwidthState to evaluate network bandwidth
Add a new state between EvaluatingPrivateDnsState and
ValidatedState to evaluate the network bandwidth.
This state is optional, OEMs can overlay the resource file and
set the related config to enable this feature.
Bug: 133522566
Test: atest NetworkStackTests
Change-Id: I4b43450ad7ed4284bf433b0daab8d0c00d4c284e
Merged-In: Ia2d6e4b8e434c15e76bc9e58874c40b1647f97fb
(cherry picked from commit 679809049207e48a63fde4f00f1084a3ea287dc1)
-rw-r--r-- | res/values/config.xml | 12 | ||||
-rw-r--r-- | res/values/overlayable.xml | 6 | ||||
-rwxr-xr-x | src/com/android/server/connectivity/NetworkMonitor.java | 177 | ||||
-rw-r--r-- | tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java | 103 |
4 files changed, 296 insertions, 2 deletions
diff --git a/res/values/config.xml b/res/values/config.xml index fdbc9db..766a807 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -85,4 +85,16 @@ failed. --> <string name="config_network_validation_failed_content_regexp" translatable="false"></string> <string name="config_network_validation_success_content_regexp" translatable="false"></string> + + <!-- URL for evaluating bandwidth. If the download cannot be finished before the timeout, then + it means the bandwidth check is failed. If the download can be finished before the timeout, + then it means the bandwidth check is passed. So the OEMs should set this URL appropriately. + --> + <string name="config_evaluating_bandwidth_url" translatable="false"></string> + <!-- A timeout for evaluating bandwidth. --> + <integer name="config_evaluating_bandwidth_timeout_ms"></integer> + <!-- The retry timer will start from config_min_retry_timer, and the timer will be exponential + increased until reaching the config_max_retry_timer. --> + <integer name="config_evaluating_bandwidth_min_retry_timer_ms"></integer> + <integer name="config_evaluating_bandwidth_max_retry_timer_ms"></integer> </resources> diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml index 7dcd663..b2967b9 100644 --- a/res/values/overlayable.xml +++ b/res/values/overlayable.xml @@ -71,6 +71,12 @@ could result in valid captive portals being incorrectly classified as having no connectivity.--> <item type="bool" name="config_force_dns_probe_private_ip_no_internet"/> + + <!-- Configurations for bandwidth check. --> + <item type="string" name="config_evaluating_bandwidth_url"/> + <item type="integer" name="config_evaluating_bandwidth_timeout_ms"/> + <item type="integer" name="config_evaluating_bandwidth_min_retry_timer_ms"/> + <item type="integer" name="config_evaluating_bandwidth_max_retry_timer_ms"/> </policy> </overlayable> </resources> diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 4c3c4bd..052fb4f 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -174,6 +174,7 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.InterruptedIOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; @@ -200,6 +201,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -364,9 +366,27 @@ public class NetworkMonitor extends StateMachine { * Message to self to poll current tcp status from kernel. */ private static final int EVENT_POLL_TCPINFO = 21; + + /** + * Message to self to do the bandwidth check in EvaluatingBandwidthState. + */ + private static final int CMD_EVALUATE_BANDWIDTH = 22; + + /** + * Message to self to know the bandwidth check is completed. + */ + private static final int CMD_BANDWIDTH_CHECK_COMPLETE = 23; + + /** + * Message to self to know the bandwidth check is timeouted. + */ + private static final int CMD_BANDWIDTH_CHECK_TIMEOUT = 24; + // 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; + // Default timeout of evaluating network bandwidth. + private static final int DEFAULT_EVALUATING_BANDWIDTH_TIMEOUT_MS = 10_000; // Before network has been evaluated this many times, ignore repeated reevaluate requests. private static final int IGNORE_REEVALUATE_ATTEMPTS = 5; private int mReevaluateToken = 0; @@ -406,6 +426,12 @@ public class NetworkMonitor extends StateMachine { private final URL[] mCaptivePortalHttpsUrls; @Nullable private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs; + // Configuration values for network bandwidth check. + @Nullable + private final String mEvaluatingBandwidthUrl; + private final int mMaxRetryTimerMs; + private final int mEvaluatingBandwidthTimeoutMs; + private final AtomicInteger mNextEvaluatingBandwidthThreadId = new AtomicInteger(1); private NetworkCapabilities mNetworkCapabilities; private LinkProperties mLinkProperties; @@ -421,6 +447,10 @@ public class NetworkMonitor extends StateMachine { private boolean mUserDoesNotWant = false; // Avoids surfacing "Sign in to network" notification. private boolean mDontDisplaySigninNotification = false; + // Set to true once the evaluating network bandwidth is passed or the captive portal respond + // APP_RETURN_WANTED_AS_IS which means the user wants to use this network anyway. + @VisibleForTesting + protected boolean mIsBandwidthCheckPassedOrIgnored = false; private final State mDefaultState = new DefaultState(); private final State mValidatedState = new ValidatedState(); @@ -430,6 +460,7 @@ public class NetworkMonitor extends StateMachine { private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState(); private final State mProbingState = new ProbingState(); private final State mWaitingForNextProbeState = new WaitingForNextProbeState(); + private final State mEvaluatingBandwidthState = new EvaluatingBandwidthState(); private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null; @@ -519,6 +550,7 @@ public class NetworkMonitor extends StateMachine { addState(mWaitingForNextProbeState, mEvaluatingState); addState(mCaptivePortalState, mMaybeNotifyState); addState(mEvaluatingPrivateDnsState, mDefaultState); + addState(mEvaluatingBandwidthState, mDefaultState); addState(mValidatedState, mDefaultState); setInitialState(mDefaultState); // CHECKSTYLE:ON IndentationCheck @@ -540,6 +572,15 @@ public class NetworkMonitor extends StateMachine { mDnsStallDetector = initDnsStallDetectorIfRequired(mDataStallEvaluationType, mConsecutiveDnsTimeoutThreshold); mTcpTracker = tst; + // Read the configurations of evaluating network bandwidth. + mEvaluatingBandwidthUrl = getResStringConfig(mContext, + R.string.config_evaluating_bandwidth_url, null); + mMaxRetryTimerMs = getResIntConfig(mContext, + R.integer.config_evaluating_bandwidth_max_retry_timer_ms, + MAX_REEVALUATE_DELAY_MS); + mEvaluatingBandwidthTimeoutMs = getResIntConfig(mContext, + R.integer.config_evaluating_bandwidth_timeout_ms, + DEFAULT_EVALUATING_BANDWIDTH_TIMEOUT_MS); // Provide empty LinkProperties and NetworkCapabilities to make sure they are never null, // even before notifyNetworkConnected. @@ -777,6 +818,9 @@ public class NetworkMonitor extends StateMachine { break; case APP_RETURN_WANTED_AS_IS: mDontDisplaySigninNotification = true; + // If the user wants to use this network anyway, there is no need to + // perform the bandwidth check even if configured. + mIsBandwidthCheckPassedOrIgnored = true; // TODO: Distinguish this from a network that actually validates. // Displaying the "x" on the system UI icon may still be a good idea. transitionTo(mEvaluatingPrivateDnsState); @@ -1270,8 +1314,12 @@ public class NetworkMonitor extends StateMachine { mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS); } - // All good! - transitionTo(mValidatedState); + if (needEvaluatingBandwidth()) { + transitionTo(mEvaluatingBandwidthState); + } else { + // All good! + transitionTo(mValidatedState); + } break; case CMD_PRIVATE_DNS_SETTINGS_CHANGED: // When settings change the reevaluation timer must be reset. @@ -1464,6 +1512,112 @@ public class NetworkMonitor extends StateMachine { } } + private final class EvaluatingBandwidthThread extends Thread { + final int mThreadId; + + EvaluatingBandwidthThread(int id) { + mThreadId = id; + } + + @Override + public void run() { + HttpURLConnection urlConnection = null; + try { + final URL url = makeURL(mEvaluatingBandwidthUrl); + urlConnection = makeProbeConnection(url, true /* followRedirects */); + // In order to exclude the time of DNS lookup, send the delay message of timeout + // here. + sendMessageDelayed(CMD_BANDWIDTH_CHECK_TIMEOUT, mEvaluatingBandwidthTimeoutMs); + readContentFromDownloadUrl(urlConnection); + } catch (InterruptedIOException e) { + // There is a timing issue that someone triggers the forcing reevaluation when + // executing the getInputStream(). The InterruptedIOException is thrown by + // Timeout#throwIfReached, it will reset the interrupt flag of Thread. So just + // return and wait for the bandwidth reevaluation, otherwise the + // CMD_BANDWIDTH_CHECK_COMPLETE will be sent. + validationLog("The thread is interrupted when executing the getInputStream()," + + " return and wait for the bandwidth reevaluation"); + return; + } catch (IOException e) { + validationLog("Evaluating bandwidth failed: " + e + ", if the thread is not" + + " interrupted, transition to validated state directly to make sure user" + + " can use wifi normally."); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + // Don't send CMD_BANDWIDTH_CHECK_COMPLETE if the IO is interrupted or timeout. + // Only send CMD_BANDWIDTH_CHECK_COMPLETE when the download is finished normally. + // Add a serial number for CMD_BANDWIDTH_CHECK_COMPLETE to prevent handling the obsolete + // CMD_BANDWIDTH_CHECK_COMPLETE. + if (!isInterrupted()) sendMessage(CMD_BANDWIDTH_CHECK_COMPLETE, mThreadId); + } + + private void readContentFromDownloadUrl(@NonNull final HttpURLConnection conn) + throws IOException { + final byte[] buffer = new byte[1000]; + final InputStream is = conn.getInputStream(); + while (!isInterrupted() && is.read(buffer) > 0) { /* read again */ } + } + } + + private class EvaluatingBandwidthState extends State { + private EvaluatingBandwidthThread mEvaluatingBandwidthThread; + private int mRetryBandwidthDelayMs; + private int mCurrentThreadId; + + @Override + public void enter() { + mRetryBandwidthDelayMs = getResIntConfig(mContext, + R.integer.config_evaluating_bandwidth_min_retry_timer_ms, + INITIAL_REEVALUATE_DELAY_MS); + sendMessage(CMD_EVALUATE_BANDWIDTH); + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case CMD_EVALUATE_BANDWIDTH: + mCurrentThreadId = mNextEvaluatingBandwidthThreadId.getAndIncrement(); + mEvaluatingBandwidthThread = new EvaluatingBandwidthThread(mCurrentThreadId); + mEvaluatingBandwidthThread.start(); + break; + case CMD_BANDWIDTH_CHECK_COMPLETE: + // Only handle the CMD_BANDWIDTH_CHECK_COMPLETE which is sent by the newest + // EvaluatingBandwidthThread. + if (mCurrentThreadId == msg.arg1) { + mIsBandwidthCheckPassedOrIgnored = true; + transitionTo(mValidatedState); + } + break; + case CMD_BANDWIDTH_CHECK_TIMEOUT: + validationLog("Evaluating bandwidth timeout!"); + mEvaluatingBandwidthThread.interrupt(); + scheduleReevaluatingBandwidth(); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + + private void scheduleReevaluatingBandwidth() { + sendMessageDelayed(obtainMessage(CMD_EVALUATE_BANDWIDTH), mRetryBandwidthDelayMs); + mRetryBandwidthDelayMs *= 2; + if (mRetryBandwidthDelayMs > mMaxRetryTimerMs) { + mRetryBandwidthDelayMs = mMaxRetryTimerMs; + } + } + + @Override + public void exit() { + mEvaluatingBandwidthThread.interrupt(); + removeMessages(CMD_EVALUATE_BANDWIDTH); + removeMessages(CMD_BANDWIDTH_CHECK_TIMEOUT); + } + } + // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at // most one per address family. This ensures we only wait up to 20 seconds for TCP connections // to complete, regardless of how many IP addresses a host has. @@ -1490,6 +1644,25 @@ public class NetworkMonitor extends StateMachine { } } + @VisibleForTesting + boolean onlyWifiTransport() { + int[] transportTypes = mNetworkCapabilities.getTransportTypes(); + return transportTypes.length == 1 + && transportTypes[0] == NetworkCapabilities.TRANSPORT_WIFI; + } + + @VisibleForTesting + boolean needEvaluatingBandwidth() { + if (mIsBandwidthCheckPassedOrIgnored + || TextUtils.isEmpty(mEvaluatingBandwidthUrl) + || !mNetworkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED) + || !onlyWifiTransport()) { + return false; + } + + return true; + } + private boolean getIsCaptivePortalCheckEnabled() { String symbol = CAPTIVE_PORTAL_MODE; int defaultValue = CAPTIVE_PORTAL_MODE_PROMPT; diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index 7cf130e..58378a0 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java @@ -27,6 +27,10 @@ import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS; import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.metrics.ValidationProbeEvent.PROBE_HTTP; import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_CONSECUTIVE_DNS_TIMEOUT_THRESHOLD; import static android.net.util.DataStallUtils.CONFIG_DATA_STALL_EVALUATION_TYPE; @@ -210,6 +214,7 @@ public class NetworkMonitorTest { private @Mock HttpURLConnection mOtherFallbackConnection; private @Mock HttpURLConnection mTestOverriddenUrlConnection; private @Mock HttpURLConnection mCapportApiConnection; + private @Mock HttpURLConnection mSpeedTestConnection; private @Mock Random mRandom; private @Mock NetworkMonitor.Dependencies mDependencies; // Mockito can't create a mock of INetworkMonitorCallbacks on Q because it can't find @@ -242,6 +247,7 @@ public class NetworkMonitorTest { private static final String TEST_CAPPORT_API_URL = "https://capport.example.com/api"; private static final String TEST_LOGIN_URL = "https://testportal.example.com/login"; 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}; @@ -475,6 +481,8 @@ public class NetworkMonitorTest { return mTestOverriddenUrlConnection; case TEST_CAPPORT_API_URL: return mCapportApiConnection; + case TEST_SPEED_TEST_URL: + return mSpeedTestConnection; default: fail("URL not mocked: " + url.toString()); return null; @@ -616,6 +624,101 @@ public class NetworkMonitorTest { } @Test + public void testOnlyWifiTransport() { + final WrappedNetworkMonitor wnm = makeMeteredNetworkMonitor(); + final NetworkCapabilities nc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addTransportType(TRANSPORT_VPN); + setNetworkCapabilities(wnm, nc); + assertFalse(wnm.onlyWifiTransport()); + nc.removeTransportType(TRANSPORT_VPN); + setNetworkCapabilities(wnm, nc); + assertTrue(wnm.onlyWifiTransport()); + } + + @Test + public void testNeedEvaluatingBandwidth() throws Exception { + // Make metered network first, the transport type is TRANSPORT_CELLULAR. That means the + // test cannot pass the condition check in needEvaluatingBandwidth(). + final WrappedNetworkMonitor wnm1 = makeMeteredNetworkMonitor(); + // Don't set the config_evaluating_bandwidth_url to make + // the condition check fail in needEvaluatingBandwidth(). + assertFalse(wnm1.needEvaluatingBandwidth()); + // Make the NetworkCapabilities to have the TRANSPORT_WIFI but it still cannot meet the + // condition check. + final NetworkCapabilities nc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI); + setNetworkCapabilities(wnm1, nc); + assertFalse(wnm1.needEvaluatingBandwidth()); + // Make the network to be non-metered wifi but it still cannot meet the condition check + // since the config_evaluating_bandwidth_url is not set. + nc.addCapability(NET_CAPABILITY_NOT_METERED); + setNetworkCapabilities(wnm1, nc); + assertFalse(wnm1.needEvaluatingBandwidth()); + // All configurations are set correctly. + doReturn(TEST_SPEED_TEST_URL).when(mResources).getString( + R.string.config_evaluating_bandwidth_url); + final WrappedNetworkMonitor wnm2 = makeMeteredNetworkMonitor(); + setNetworkCapabilities(wnm2, nc); + assertTrue(wnm2.needEvaluatingBandwidth()); + // Set mIsBandwidthCheckPassedOrIgnored to true and expect needEvaluatingBandwidth() will + // return false. + wnm2.mIsBandwidthCheckPassedOrIgnored = true; + assertFalse(wnm2.needEvaluatingBandwidth()); + // Reset mIsBandwidthCheckPassedOrIgnored back to false. + wnm2.mIsBandwidthCheckPassedOrIgnored = false; + // Shouldn't evaluate network bandwidth on the metered wifi. + nc.removeCapability(NET_CAPABILITY_NOT_METERED); + setNetworkCapabilities(wnm2, nc); + assertFalse(wnm2.needEvaluatingBandwidth()); + // Shouldn't evaluate network bandwidth on the unmetered cellular. + nc.addCapability(NET_CAPABILITY_NOT_METERED); + nc.removeTransportType(TRANSPORT_WIFI); + nc.addTransportType(TRANSPORT_CELLULAR); + assertFalse(wnm2.needEvaluatingBandwidth()); + } + + @Test + public void testEvaluatingBandwidthState_meteredNetwork() throws Exception { + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + final NetworkCapabilities meteredCap = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET); + doReturn(TEST_SPEED_TEST_URL).when(mResources).getString( + R.string.config_evaluating_bandwidth_url); + final NetworkMonitor nm = runNetworkTest(TEST_LINK_PROPERTIES, meteredCap, + NETWORK_VALIDATION_RESULT_VALID, NETWORK_VALIDATION_PROBE_DNS + | NETWORK_VALIDATION_PROBE_HTTPS, null /* redirectUrl */); + // Evaluating bandwidth process won't be executed when the network is metered wifi. + // Check that the connection hasn't been opened and the state should transition to validated + // state directly. + verify(mCleartextDnsNetwork, never()).openConnection(new URL(TEST_SPEED_TEST_URL)); + assertEquals(NETWORK_VALIDATION_RESULT_VALID, + nm.getEvaluationState().getEvaluationResult()); + } + + @Test + public void testEvaluatingBandwidthState_nonMeteredNetworkWithWrongConfig() throws Exception { + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + final NetworkCapabilities nonMeteredCap = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_METERED); + doReturn("").when(mResources).getString(R.string.config_evaluating_bandwidth_url); + final NetworkMonitor nm = runNetworkTest(TEST_LINK_PROPERTIES, nonMeteredCap, + NETWORK_VALIDATION_RESULT_VALID, NETWORK_VALIDATION_PROBE_DNS + | NETWORK_VALIDATION_PROBE_HTTPS, null /* redirectUrl */); + // Non-metered network with wrong configuration(the config_evaluating_bandwidth_url is + // empty). Check that the connection hasn't been opened and the state should transition to + // validated state directly. + verify(mCleartextDnsNetwork, never()).openConnection(new URL(TEST_SPEED_TEST_URL)); + assertEquals(NETWORK_VALIDATION_RESULT_VALID, + nm.getEvaluationState().getEvaluationResult()); + } + + @Test public void testMatchesHttpContent() throws Exception { final WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor(); doReturn("[\\s\\S]*line2[\\s\\S]*").when(mResources).getString( |