summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Lin <lucaslin@google.com>2020-05-07 06:25:14 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2020-05-07 06:25:14 +0000
commitb12790b92269c9810122c414d77a592f9f17cc35 (patch)
treeacc651b013139d121ba2c03053b41517e928264f
parent49d1392ef671e28129d4441d09a2b541f4a802c2 (diff)
parentfc5814c128a03e495b62731b2801f7b661026dce (diff)
Merge "Add EvaluatingBandwidthState to evaluate network bandwidth" into rvc-dev
-rw-r--r--res/values/config.xml12
-rw-r--r--res/values/overlayable.xml6
-rwxr-xr-xsrc/com/android/server/connectivity/NetworkMonitor.java177
-rw-r--r--tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java103
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(