summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRemi NGUYEN VAN <reminv@google.com>2020-07-06 03:26:08 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-07-06 03:26:08 +0000
commite1cabba716f66dd8197f0c19bb6502af8cae9c24 (patch)
treea712704cd02d33c840f4135ca896ed0cd3cdbc1f
parentcc94da9a6a1713efd3f867b80ce0441a1bb6057c (diff)
parenta0428bfc7c2b1eb1b2a9f9252378e5e93ff64ba1 (diff)
Merge "Injecting network validation stats into statsd" into rvc-dev am: e1b31f1058 am: a0428bfc7c
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/NetworkStack/+/11952741 Change-Id: I717fd85f92ae722c201befba9a9825b76bd88ef5
-rw-r--r--apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java10
-rw-r--r--apishim/common/com/android/networkstack/apishim/common/CaptivePortalDataShim.java10
-rw-r--r--src/com/android/networkstack/metrics/NetworkValidationMetrics.java220
-rwxr-xr-xsrc/com/android/server/connectivity/NetworkMonitor.java94
-rw-r--r--tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java249
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());
+ }
+}