diff options
author | Frank Li <lifr@google.com> | 2020-06-22 12:45:27 +0000 |
---|---|---|
committer | Remi NGUYEN VAN <reminv@google.com> | 2020-07-03 14:56:43 +0900 |
commit | e04f8b7dfb9d7efa7f87de3859014d3e7eb524f2 (patch) | |
tree | fde057f1d2620e1dd9c9c6ad7351ce08b537cfd0 | |
parent | b002fd1e07d040b9c08852f63cfee2dbb7e811fe (diff) |
Injecting network validation stats into statsd
1. Fill in each field of the NetworkValidationReported
2. Write the NetworkValidationReported into statsd
This patch also refactors tryCapportApiProbe to return null when the
capport data is incorrect, instead of doing the check after calling the
method. This makes it easier to compile capport API probe metrics.
Test: atest NetworkStackIntegrationTests NetworkStackTests
Test: atest FrameworksNetTests
Test: Manual test with statsd_testdrive
Bug: 151796056
Original-Change: https://android-review.googlesource.com/1295496
Merged-In: Icf34402d6a293cc76c32d00835cbf358c99a87fa
Change-Id: Icf34402d6a293cc76c32d00835cbf358c99a87fa
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()); + } +} |