diff options
author | Lucas Lin <lucaslin@google.com> | 2020-05-07 06:25:14 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-05-07 06:25:14 +0000 |
commit | b12790b92269c9810122c414d77a592f9f17cc35 (patch) | |
tree | acc651b013139d121ba2c03053b41517e928264f | |
parent | 49d1392ef671e28129d4441d09a2b541f4a802c2 (diff) | |
parent | fc5814c128a03e495b62731b2801f7b661026dce (diff) |
Merge "Add EvaluatingBandwidthState to evaluate network bandwidth" into rvc-dev
-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( |