diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2020-07-06 03:00:21 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-07-06 03:00:21 +0000 |
commit | e1b31f105824614c6a506a99faed954d0d6214d3 (patch) | |
tree | 7b435896c128df15885d91598c357e646f504e04 | |
parent | 442cffc1763fa0147ea2d30e36ef7b37ee57b38e (diff) | |
parent | e04f8b7dfb9d7efa7f87de3859014d3e7eb524f2 (diff) |
Merge "Injecting network validation stats into statsd" into rvc-dev
5 files changed, 571 insertions, 12 deletions
diff --git a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java index 0c75f27..5639386 100644 --- a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java +++ b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java @@ -93,6 +93,16 @@ public class CaptivePortalDataShimImpl } @Override + public long getByteLimit() { + return mData.getByteLimit(); + } + + @Override + public long getExpiryTimeMillis() { + return mData.getExpiryTimeMillis(); + } + + @Override public Uri getUserPortalUrl() { return mData.getUserPortalUrl(); } diff --git a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java index fe99c13..a18ba49 100644 --- a/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java +++ b/apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java @@ -30,6 +30,16 @@ public interface CaptivePortalDataShim { boolean isCaptive(); /** + * @see android.net.CaptivePortalData#getByteLimit() + */ + long getByteLimit(); + + /** + * @see android.net.CaptivePortalData#getExpiryTimeMillis() + */ + long getExpiryTimeMillis(); + + /** * @see android.net.CaptivePortalData#getUserPortalUrl() */ Uri getUserPortalUrl(); diff --git a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java new file mode 100644 index 0000000..3789c5c --- /dev/null +++ b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.metrics; + +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; + +import static java.lang.System.currentTimeMillis; + +import android.net.INetworkMonitor; +import android.net.NetworkCapabilities; +import android.net.captiveportal.CaptivePortalProbeResult; +import android.net.metrics.ValidationProbeEvent; +import android.net.util.NetworkStackUtils; +import android.net.util.Stopwatch; +import android.stats.connectivity.ProbeResult; +import android.stats.connectivity.ProbeType; +import android.stats.connectivity.TransportType; +import android.stats.connectivity.ValidationResult; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.networkstack.apishim.common.CaptivePortalDataShim; + +/** + * Class to record the network validation into statsd. + * 1. Fill in NetworkValidationReported proto. + * 2. Write the NetworkValidationReported proto into statsd. + * @hide + */ + +public class NetworkValidationMetrics { + private final NetworkValidationReported.Builder mStatsBuilder = + NetworkValidationReported.newBuilder(); + private final ProbeEvents.Builder mProbeEventsBuilder = ProbeEvents.newBuilder(); + private final CapportApiData.Builder mCapportApiDataBuilder = CapportApiData.newBuilder(); + private final Stopwatch mWatch = new Stopwatch(); + private int mValidationIndex = 0; + // Define a maximum size that can store events. + public static final int MAX_PROBE_EVENTS_COUNT = 20; + + /** + * Reset this NetworkValidationMetrics. + */ + public void reset(@Nullable NetworkCapabilities nc) { + mStatsBuilder.clear(); + mProbeEventsBuilder.clear(); + mCapportApiDataBuilder.clear(); + mWatch.restart(); + mStatsBuilder.setTransportType(getTransportTypeFromNC(nc)); + mValidationIndex++; + } + + /** + * Returns the enum TransportType + * + * @param NetworkCapabilities + * @return the TransportType which is defined in + * core/proto/android/stats/connectivity/network_stack.proto + */ + @VisibleForTesting + public static TransportType getTransportTypeFromNC( + @Nullable NetworkCapabilities nc) { + if (nc == null) return TransportType.TT_UNKNOWN; + boolean hasCellular = nc.hasTransport(TRANSPORT_CELLULAR); + boolean hasWifi = nc.hasTransport(TRANSPORT_WIFI); + boolean hasBT = nc.hasTransport(TRANSPORT_BLUETOOTH); + boolean hasEthernet = nc.hasTransport(TRANSPORT_ETHERNET); + boolean hasVpn = nc.hasTransport(TRANSPORT_VPN); + boolean hasWifiAware = nc.hasTransport(TRANSPORT_WIFI_AWARE); + boolean hasLopan = nc.hasTransport(TRANSPORT_LOWPAN); + + if (hasCellular && hasWifi && hasVpn) return TransportType.TT_WIFI_CELLULAR_VPN; + if (hasWifi) return hasVpn ? TransportType.TT_WIFI_VPN : TransportType.TT_WIFI; + if (hasCellular) return hasVpn ? TransportType.TT_CELLULAR_VPN : TransportType.TT_CELLULAR; + if (hasBT) return hasVpn ? TransportType.TT_BLUETOOTH_VPN : TransportType.TT_BLUETOOTH; + if (hasEthernet) return hasVpn ? TransportType.TT_ETHERNET_VPN : TransportType.TT_ETHERNET; + if (hasWifiAware) return TransportType.TT_WIFI_AWARE; + if (hasLopan) return TransportType.TT_LOWPAN; + return TransportType.TT_UNKNOWN; + } + + /** + * Map {@link ValidationProbeEvent} to {@link ProbeType}. + */ + public static ProbeType probeTypeToEnum(final int probeType) { + switch(probeType) { + case ValidationProbeEvent.PROBE_DNS: + return ProbeType.PT_DNS; + case ValidationProbeEvent.PROBE_HTTP: + return ProbeType.PT_HTTP; + case ValidationProbeEvent.PROBE_HTTPS: + return ProbeType.PT_HTTPS; + case ValidationProbeEvent.PROBE_PAC: + return ProbeType.PT_PAC; + case ValidationProbeEvent.PROBE_FALLBACK: + return ProbeType.PT_FALLBACK; + case ValidationProbeEvent.PROBE_PRIVDNS: + return ProbeType.PT_PRIVDNS; + default: + return ProbeType.PT_UNKNOWN; + } + } + + /** + * Map {@link CaptivePortalProbeResult} to {@link ProbeResult}. + */ + public static ProbeResult httpProbeResultToEnum(final CaptivePortalProbeResult result) { + if (result == null) return ProbeResult.PR_UNKNOWN; + + if (result.isSuccessful()) { + return ProbeResult.PR_SUCCESS; + } else if (result.isDnsPrivateIpResponse()) { + return ProbeResult.PR_PRIVATE_IP_DNS; + } else if (result.isFailed()) { + return ProbeResult.PR_FAILURE; + } else if (result.isPortal()) { + return ProbeResult.PR_PORTAL; + } else { + return ProbeResult.PR_UNKNOWN; + } + } + + /** + * Map validation result (as per INetworkMonitor) to {@link ValidationResult}. + */ + @VisibleForTesting + public static ValidationResult validationResultToEnum(int result, String redirectUrl) { + if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID) != 0) { + return ValidationResult.VR_SUCCESS; + } else if (redirectUrl != null) { + return ValidationResult.VR_PORTAL; + } else if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL) != 0) { + return ValidationResult.VR_PARTIAL; + } else { + return ValidationResult.VR_FAILURE; + } + } + + /** + * Write each network probe event to mProbeEventsBuilder. + */ + public void setProbeEvent(final ProbeType type, final long durationUs, final ProbeResult result, + @Nullable final CaptivePortalDataShim capportData) { + // When the number of ProbeEvents of mProbeEventsBuilder exceeds + // MAX_PROBE_EVENTS_COUNT, stop adding ProbeEvent. + if (mProbeEventsBuilder.getProbeEventCount() >= MAX_PROBE_EVENTS_COUNT) return; + + int latencyUs = NetworkStackUtils.saturatedCast(durationUs); + + final ProbeEvent.Builder probeEventBuilder = ProbeEvent.newBuilder() + .setLatencyMicros(latencyUs) + .setProbeType(type) + .setProbeResult(result); + + if (capportData != null) { + final long secondsRemaining = + (capportData.getExpiryTimeMillis() - currentTimeMillis()) / 1000; + mCapportApiDataBuilder + .setRemainingTtlSecs(NetworkStackUtils.saturatedCast(secondsRemaining)) + .setRemainingBytes(NetworkStackUtils.saturatedCast(capportData.getByteLimit())) + .setHasPortalUrl((capportData.getUserPortalUrl() != null)) + .setHasVenueInfo((capportData.getVenueInfoUrl() != null)); + probeEventBuilder.setCapportApiData(mCapportApiDataBuilder); + } + + mProbeEventsBuilder.addProbeEvent(probeEventBuilder); + } + + /** + * Write the network validation info to mStatsBuilder. + */ + public void setValidationResult(int result, String redirectUrl) { + mStatsBuilder.setValidationResult(validationResultToEnum(result, redirectUrl)); + } + + /** + * Write the NetworkValidationReported proto to statsd. + */ + public NetworkValidationReported sendValidationStats() { + if (!mWatch.isStarted()) return null; + mStatsBuilder.setProbeEvents(mProbeEventsBuilder); + mStatsBuilder.setLatencyMicros(NetworkStackUtils.saturatedCast(mWatch.stop())); + mStatsBuilder.setValidationIndex(mValidationIndex); + // write a random value(0 ~ 999) for sampling. + mStatsBuilder.setRandomNumber((int) (Math.random() * 1000)); + final NetworkValidationReported mStats = mStatsBuilder.build(); + final byte[] probeEvents = mStats.getProbeEvents().toByteArray(); + + NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_VALIDATION_REPORTED, + mStats.getTransportType().getNumber(), + probeEvents, + mStats.getValidationResult().getNumber(), + mStats.getLatencyMicros(), + mStats.getValidationIndex(), + mStats.getRandomNumber()); + mWatch.reset(); + return mStats; + } +} diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index b6b1fff..47c910a 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -129,6 +129,8 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; +import android.stats.connectivity.ProbeResult; +import android.stats.connectivity.ProbeType; import android.telephony.AccessNetworkConstants; import android.telephony.CellIdentityNr; import android.telephony.CellInfo; @@ -155,6 +157,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -168,6 +171,7 @@ import com.android.networkstack.apishim.common.ShimUtils; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; +import com.android.networkstack.metrics.NetworkValidationMetrics; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; import com.android.server.NetworkStackService.NetworkStackServiceManager; @@ -500,6 +504,10 @@ public class NetworkMonitor extends StateMachine { private final boolean mPrivateIpNoInternetEnabled; + @GuardedBy("mNetworkValidationMetrics") + private final NetworkValidationMetrics mNetworkValidationMetrics = + new NetworkValidationMetrics(); + private int getCallbackVersion(INetworkMonitorCallbacks cb) { int version; try { @@ -775,6 +783,31 @@ public class NetworkMonitor extends StateMachine { } } + private void recordMetricsReset(@Nullable NetworkCapabilities nc) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.reset(nc); + } + } + + private void recordMetricsProbeEvent(ProbeType type, long latencyMicros, ProbeResult result, + CaptivePortalDataShim capportData) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.setProbeEvent(type, latencyMicros, result, capportData); + } + } + + private void recordMetricsValidationResult(int result, String redirectUrl) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.setValidationResult(result, redirectUrl); + } + } + + private void recordMetricsValidationStats() { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.sendValidationStats(); + } + } + // DefaultState is the parent of all States. It exists only to handle CMD_* messages but // does not entail any real state (hence no enter() or exit() routines). private class DefaultState extends State { @@ -787,6 +820,7 @@ public class NetworkMonitor extends StateMachine { transitionTo(mEvaluatingState); return HANDLED; case CMD_NETWORK_DISCONNECTED: + recordMetricsValidationStats(); logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED); quit(); return HANDLED; @@ -938,6 +972,7 @@ public class NetworkMonitor extends StateMachine { initSocketTrackingIfRequired(); // start periodical polling. sendTcpPollingEvent(); + recordMetricsValidationStats(); } private void initSocketTrackingIfRequired() { @@ -957,6 +992,9 @@ public class NetworkMonitor extends StateMachine { transitionTo(mValidatedState); break; case CMD_EVALUATE_PRIVATE_DNS: + // TODO: this causes reevaluation of a single probe that is not counted in + // metrics. Add support for such reevaluation probes in metrics, and log them + // separately. transitionTo(mEvaluatingPrivateDnsState); break; case EVENT_DNS_NOTIFICATION: @@ -1285,6 +1323,7 @@ public class NetworkMonitor extends StateMachine { sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */, CAPTIVE_PORTAL_REEVALUATE_DELAY_MS); mValidations++; + recordMetricsValidationStats(); } @Override @@ -1316,6 +1355,10 @@ public class NetworkMonitor extends StateMachine { notifyPrivateDnsConfigResolved(); } else { handlePrivateDnsEvaluationFailure(); + // The private DNS probe fails-fast if the server hostname cannot + // be resolved. Record it as a failure with zero latency. + recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, 0 /* latency */, + ProbeResult.PR_FAILURE, null /* capportData */); break; } } @@ -1425,6 +1468,8 @@ public class NetworkMonitor extends StateMachine { validationLog(PROBE_PRIVDNS, host, String.format("%dus - Error: %s", time, uhe.getMessage())); } + recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, time, success ? ProbeResult.PR_SUCCESS : + ProbeResult.PR_FAILURE, null /* capportData */); logValidationProbe(time, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE); return success; } @@ -1435,6 +1480,8 @@ public class NetworkMonitor extends StateMachine { @Override public void enter() { + recordMetricsValidationStats(); + recordMetricsReset(mNetworkCapabilities); if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) { //Don't continue to blame UID forever. TrafficStats.clearThreadStatsUid(); @@ -1516,6 +1563,7 @@ public class NetworkMonitor extends StateMachine { private class WaitingForNextProbeState extends State { @Override public void enter() { + recordMetricsValidationStats(); scheduleNextProbe(); } @@ -2265,6 +2313,8 @@ public class NetworkMonitor extends StateMachine { // network validation (the HTTPS probe, which would likely fail anyway) or the PAC probe. if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP && (proxy == null) && hasPrivateIpAddress(resolvedAddr)) { + recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType), + 0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */); return CaptivePortalProbeResult.PRIVATE_IP; } return sendHttpProbe(url, probeType, null); @@ -2296,6 +2346,9 @@ public class NetworkMonitor extends StateMachine { result = ValidationProbeEvent.DNS_FAILURE; } final long latency = watch.stop(); + recordMetricsProbeEvent(ProbeType.PT_DNS, latency, + (result == ValidationProbeEvent.DNS_SUCCESS) ? ProbeResult.PR_SUCCESS : + ProbeResult.PR_FAILURE, null /* capportData */); logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result); return addresses; } @@ -2413,12 +2466,17 @@ public class NetworkMonitor extends StateMachine { } logValidationProbe(probeTimer.stop(), probeType, httpResponseCode); + final CaptivePortalProbeResult probeResult; if (probeSpec == null) { - return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString(), - 1 << probeType); + probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl, + url.toString(), 1 << probeType); } else { - return probeSpec.getResult(httpResponseCode, redirectUrl); + probeResult = probeSpec.getResult(httpResponseCode, redirectUrl); } + recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType), + probeTimer.stop(), NetworkValidationMetrics.httpProbeResultToEnum(probeResult), + null /* capportData */); + return probeResult; } @VisibleForTesting @@ -2570,8 +2628,7 @@ public class NetworkMonitor extends StateMachine { super(deps, proxy, url, captivePortalApiUrl); } - private CaptivePortalDataShim tryCapportApiProbe() { - if (mCaptivePortalApiUrl == null) return null; + private CaptivePortalDataShim sendCapportApiProbe() { validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl); final String apiContent; @@ -2610,26 +2667,38 @@ public class NetworkMonitor extends StateMachine { try { final JSONObject info = new JSONObject(apiContent); - return CaptivePortalDataShimImpl.fromJson(info); + final CaptivePortalDataShim capportData = CaptivePortalDataShimImpl.fromJson(info); + if (capportData != null && capportData.isCaptive() + && capportData.getUserPortalUrl() == null) { + validationLog("Missing user-portal-url from capport response"); + return null; + } + return capportData; } catch (JSONException e) { validationLog("Could not parse capport API JSON: " + e.getMessage()); return null; } catch (UnsupportedApiLevelException e) { + // This should never happen because LinkProperties would not have a capport URL + // before R. validationLog("Platform API too low to support capport API"); return null; } } + private CaptivePortalDataShim tryCapportApiProbe() { + if (mCaptivePortalApiUrl == null) return null; + final Stopwatch capportApiWatch = new Stopwatch().start(); + final CaptivePortalDataShim capportData = sendCapportApiProbe(); + recordMetricsProbeEvent(ProbeType.PT_CAPPORT_API, capportApiWatch.stop(), + capportData == null ? ProbeResult.PR_FAILURE : ProbeResult.PR_SUCCESS, + capportData); + return capportData; + } + @Override protected CaptivePortalProbeResult sendProbe() { final CaptivePortalDataShim capportData = tryCapportApiProbe(); if (capportData != null && capportData.isCaptive()) { - if (capportData.getUserPortalUrl() == null) { - validationLog("Missing user-portal-url from capport response"); - return new CapportApiProbeResult( - sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTP), - null /* capportData */); - } final String loginUrlString = capportData.getUserPortalUrl().toString(); // Starting from R (where CaptivePortalData was introduced), the captive portal app // delegates to NetworkMonitor for verifying when the network validates instead of @@ -3343,6 +3412,7 @@ public class NetworkMonitor extends StateMachine { p.redirectUrl = redirectUrl; p.timestampMillis = SystemClock.elapsedRealtime(); notifyNetworkTested(p); + recordMetricsValidationResult(result, redirectUrl); } @VisibleForTesting diff --git a/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java new file mode 100644 index 0000000..af68cde --- /dev/null +++ b/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.metrics; + +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import android.net.INetworkMonitor; +import android.net.NetworkCapabilities; +import android.net.captiveportal.CaptivePortalProbeResult; +import android.net.metrics.ValidationProbeEvent; +import android.stats.connectivity.ProbeResult; +import android.stats.connectivity.ProbeType; +import android.stats.connectivity.TransportType; +import android.stats.connectivity.ValidationResult; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.networkstack.apishim.CaptivePortalDataShimImpl; +import com.android.networkstack.apishim.common.CaptivePortalDataShim; + +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkValidationMetricsTest { + 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 int TTL_TOLERANCE_SECS = 10; + + private static final NetworkCapabilities WIFI_NOT_METERED_CAPABILITIES = + new NetworkCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + + @Test + public void testNetworkValidationMetrics_VerifyProbeTypeToEnum() throws Exception { + verifyProbeType(ValidationProbeEvent.PROBE_DNS, ProbeType.PT_DNS); + verifyProbeType(ValidationProbeEvent.PROBE_HTTP, ProbeType.PT_HTTP); + verifyProbeType(ValidationProbeEvent.PROBE_HTTPS, ProbeType.PT_HTTPS); + verifyProbeType(ValidationProbeEvent.PROBE_PAC, ProbeType.PT_PAC); + verifyProbeType(ValidationProbeEvent.PROBE_FALLBACK, ProbeType.PT_FALLBACK); + verifyProbeType(ValidationProbeEvent.PROBE_PRIVDNS, ProbeType.PT_PRIVDNS); + } + + private void verifyProbeType(int inputProbeType, ProbeType expectedEnumType) { + assertEquals(expectedEnumType, NetworkValidationMetrics.probeTypeToEnum(inputProbeType)); + } + + @Test + public void testNetworkValidationMetrics_VerifyHttpProbeResultToEnum() throws Exception { + verifyProbeType(new CaptivePortalProbeResult(CaptivePortalProbeResult.SUCCESS_CODE, + ValidationProbeEvent.PROBE_HTTP), ProbeResult.PR_SUCCESS); + verifyProbeType(new CaptivePortalProbeResult(CaptivePortalProbeResult.FAILED_CODE, + ValidationProbeEvent.PROBE_HTTP), ProbeResult.PR_FAILURE); + verifyProbeType(new CaptivePortalProbeResult(CaptivePortalProbeResult.PORTAL_CODE, + ValidationProbeEvent.PROBE_HTTP), ProbeResult.PR_PORTAL); + verifyProbeType(CaptivePortalProbeResult.PRIVATE_IP, ProbeResult.PR_PRIVATE_IP_DNS); + } + + private void verifyProbeType(CaptivePortalProbeResult inputResult, + ProbeResult expectedResult) { + assertEquals(expectedResult, NetworkValidationMetrics.httpProbeResultToEnum(inputResult)); + } + + @Test + public void testNetworkValidationMetrics_VerifyValidationResultToEnum() throws Exception { + verifyProbeType(INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID, null, + ValidationResult.VR_SUCCESS); + verifyProbeType(0, TEST_LOGIN_URL, ValidationResult.VR_PORTAL); + verifyProbeType(INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL, null, + ValidationResult.VR_PARTIAL); + verifyProbeType(0, null, ValidationResult.VR_FAILURE); + } + + private void verifyProbeType(int inputResult, String inputRedirectUrl, + ValidationResult expectedResult) { + assertEquals(expectedResult, NetworkValidationMetrics.validationResultToEnum(inputResult, + inputRedirectUrl)); + } + + @Test + public void testNetworkValidationMetrics_VerifyTransportTypeToEnum() throws Exception { + final NetworkValidationMetrics metrics = new NetworkValidationMetrics(); + NetworkCapabilities nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_WIFI); + assertEquals(TransportType.TT_WIFI, metrics.getTransportTypeFromNC(nc)); + nc.addTransportType(TRANSPORT_VPN); + assertEquals(TransportType.TT_WIFI_VPN, metrics.getTransportTypeFromNC(nc)); + nc.addTransportType(TRANSPORT_CELLULAR); + assertEquals(TransportType.TT_WIFI_CELLULAR_VPN, metrics.getTransportTypeFromNC(nc)); + + nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_CELLULAR); + assertEquals(TransportType.TT_CELLULAR, metrics.getTransportTypeFromNC(nc)); + nc.addTransportType(TRANSPORT_VPN); + assertEquals(TransportType.TT_CELLULAR_VPN, metrics.getTransportTypeFromNC(nc)); + + nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_BLUETOOTH); + assertEquals(TransportType.TT_BLUETOOTH, metrics.getTransportTypeFromNC(nc)); + nc.addTransportType(TRANSPORT_VPN); + assertEquals(TransportType.TT_BLUETOOTH_VPN, metrics.getTransportTypeFromNC(nc)); + + nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_ETHERNET); + assertEquals(TransportType.TT_ETHERNET, metrics.getTransportTypeFromNC(nc)); + nc.addTransportType(TRANSPORT_VPN); + assertEquals(TransportType.TT_ETHERNET_VPN, metrics.getTransportTypeFromNC(nc)); + } + + @Test + public void testNetworkValidationMetrics_VerifyConsecutiveProbeFailure() throws Exception { + final NetworkValidationMetrics Metrics = new NetworkValidationMetrics(); + Metrics.reset(WIFI_NOT_METERED_CAPABILITIES); + // 1. PT_DNS probe + Metrics.setProbeEvent(ProbeType.PT_DNS, 1234, ProbeResult.PR_SUCCESS, null); + // 2. Consecutive PT_HTTP probe failure + for (int i = 0; i < 30; i++) { + Metrics.setProbeEvent(ProbeType.PT_HTTP, 1234, ProbeResult.PR_FAILURE, null); + } + + // Write metric into statsd + final NetworkValidationReported stats = Metrics.sendValidationStats(); + + // The maximum number of probe records should be the same as MAX_PROBE_EVENTS_COUNT + final ProbeEvents probeEvents = stats.getProbeEvents(); + assertEquals(NetworkValidationMetrics.MAX_PROBE_EVENTS_COUNT, + probeEvents.getProbeEventCount()); + } + + @Test + public void testNetworkValidationMetrics_VerifyCollectMetrics() throws Exception { + assumeTrue(CaptivePortalDataShimImpl.isSupported()); + final long bytesRemaining = 10L; + final long secondsRemaining = 3000L; + String apiContent = "{'captive': true," + + "'user-portal-url': '" + TEST_LOGIN_URL + "'," + + "'venue-info-url': '" + TEST_VENUE_INFO_URL + "'," + + "'bytes-remaining': " + bytesRemaining + "," + + "'seconds-remaining': " + secondsRemaining + "}"; + final NetworkValidationMetrics Metrics = new NetworkValidationMetrics(); + final int validationIndex = 1; + final long longlatency = 2147483649L; + Metrics.reset(WIFI_NOT_METERED_CAPABILITIES); + + final JSONObject info = new JSONObject(apiContent); + final CaptivePortalDataShim captivePortalData = + CaptivePortalDataShimImpl.fromJson(info); + + // 1. PT_CAPPORT_API probe w CapportApiData info + Metrics.setProbeEvent(ProbeType.PT_CAPPORT_API, 1234, ProbeResult.PR_SUCCESS, + captivePortalData); + // 2. PT_CAPPORT_API probe w/o CapportApiData info + Metrics.setProbeEvent(ProbeType.PT_CAPPORT_API, 1234, ProbeResult.PR_FAILURE, null); + + // 3. PT_DNS probe + Metrics.setProbeEvent(ProbeType.PT_DNS, 5678, ProbeResult.PR_FAILURE, null); + + // 4. PT_HTTP probe + Metrics.setProbeEvent(ProbeType.PT_HTTP, longlatency, ProbeResult.PR_PORTAL, null); + + // add Validation result + Metrics.setValidationResult(INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL, null); + + // Write metric into statsd + final NetworkValidationReported stats = Metrics.sendValidationStats(); + + // Verify: TransportType: WIFI + assertEquals(TransportType.TT_WIFI, stats.getTransportType()); + + // Verify: validationIndex + assertEquals(validationIndex, stats.getValidationIndex()); + + // probe Count: 4 (PT_CAPPORT_API, PT_DNS, PT_HTTP, PT_CAPPORT_API) + final ProbeEvents probeEvents = stats.getProbeEvents(); + assertEquals(4, probeEvents.getProbeEventCount()); + + // Verify the 1st probe: ProbeType = PT_CAPPORT_API, Latency_us = 1234, + // ProbeResult = PR_SUCCESS, CapportApiData = capportData + ProbeEvent probeEvent = probeEvents.getProbeEvent(0); + assertEquals(ProbeType.PT_CAPPORT_API, probeEvent.getProbeType()); + assertEquals(1234, probeEvent.getLatencyMicros()); + assertEquals(ProbeResult.PR_SUCCESS, probeEvent.getProbeResult()); + assertEquals(true, probeEvent.hasCapportApiData()); + if (CaptivePortalDataShimImpl.isSupported()) { + // Set secondsRemaining to 3000 and check that getRemainingTtlSecs is within 10 seconds + final CapportApiData capportData = probeEvent.getCapportApiData(); + assertTrue(capportData.getRemainingTtlSecs() <= secondsRemaining); + assertTrue(capportData.getRemainingTtlSecs() + TTL_TOLERANCE_SECS > secondsRemaining); + assertEquals(captivePortalData.getByteLimit(), capportData.getRemainingBytes()); + } else { + assertFalse(probeEvent.hasCapportApiData()); + } + + // Verify the 2nd probe: ProbeType = PT_CAPPORT_API, Latency_us = 1234, + // ProbeResult = PR_SUCCESS, CapportApiData = null + probeEvent = probeEvents.getProbeEvent(1); + assertEquals(ProbeType.PT_CAPPORT_API, probeEvent.getProbeType()); + assertEquals(1234, probeEvent.getLatencyMicros()); + assertEquals(ProbeResult.PR_FAILURE, probeEvent.getProbeResult()); + assertEquals(false, probeEvent.hasCapportApiData()); + + // Verify the 3rd probe: ProbeType = PT_DNS, Latency_us = 5678, + // ProbeResult = PR_FAILURE, CapportApiData = null + probeEvent = probeEvents.getProbeEvent(2); + assertEquals(ProbeType.PT_DNS, probeEvent.getProbeType()); + assertEquals(5678, probeEvent.getLatencyMicros()); + assertEquals(ProbeResult.PR_FAILURE, probeEvent.getProbeResult()); + assertEquals(false, probeEvent.hasCapportApiData()); + + // Verify the 4th probe: ProbeType = PT_HTTP, Latency_us = longlatency, + // ProbeResult = PR_PORTAL, CapportApiData = null + probeEvent = probeEvents.getProbeEvent(3); + assertEquals(ProbeType.PT_HTTP, probeEvent.getProbeType()); + // The latency exceeds Integer.MAX_VALUE(2147483647), it is limited to Integer.MAX_VALUE + assertEquals(Integer.MAX_VALUE, probeEvent.getLatencyMicros()); + assertEquals(ProbeResult.PR_PORTAL, probeEvent.getProbeResult()); + assertEquals(false, probeEvent.hasCapportApiData()); + + // Verify the ValidationResult + assertEquals(ValidationResult.VR_PARTIAL, stats.getValidationResult()); + } +} |