Snap for 6657150 from bf45240a2ac9dc17240bfff6596aee64e1d70a28 to rvc-release
Change-Id: I44acb557f9b07f858f3e24f2b79c6ae15f2d4a36
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 @@
}
@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 @@
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/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index 19ca4b5..94de7c3 100755
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -226,6 +226,15 @@
public static final String DNS_PROBE_PRIVATE_IP_NO_INTERNET_VERSION =
"dns_probe_private_ip_no_internet";
+ /**
+ * Experiment flag to enable validation metrics sent by NetworkMonitor.
+ *
+ * Metrics are sent by default. They can be disabled by setting the flag to a number greater
+ * than the APK version (for example 999999999).
+ * @see #isFeatureEnabled(Context, String, String, boolean)
+ */
+ public static final String VALIDATION_METRICS_VERSION = "validation_metrics_version";
+
static {
System.loadLibrary("networkstackutilsjni");
}
@@ -348,6 +357,9 @@
* {@link DeviceConfig} is enabled by comparing NetworkStack module version {@link NetworkStack}
* with current version of property. If this property version is valid, the corresponding
* experimental feature would be enabled, otherwise disabled.
+ *
+ * This is useful to ensure that if a module install is rolled back, flags are not left fully
+ * rolled out on a version where they have not been well tested.
* @param context The global context information about an app environment.
* @param namespace The namespace containing the property to look up.
* @param name The name of the property to look up.
@@ -363,6 +375,9 @@
* {@link DeviceConfig} is enabled by comparing NetworkStack module version {@link NetworkStack}
* with current version of property. If this property version is valid, the corresponding
* experimental feature would be enabled, otherwise disabled.
+ *
+ * This is useful to ensure that if a module install is rolled back, flags are not left fully
+ * rolled out on a version where they have not been well tested.
* @param context The global context information about an app environment.
* @param namespace The namespace containing the property to look up.
* @param name The name of the property to look up.
diff --git a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
new file mode 100644
index 0000000..f27a939
--- /dev/null
+++ b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
@@ -0,0 +1,254 @@
+/*
+ * 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 and start collecting timing and metrics.
+ *
+ * <p>This must be called when validation starts.
+ */
+ public void startCollection(@Nullable NetworkCapabilities nc) {
+ mStatsBuilder.clear();
+ mProbeEventsBuilder.clear();
+ mCapportApiDataBuilder.clear();
+ mWatch.restart();
+ mStatsBuilder.setTransportType(getTransportTypeFromNC(nc));
+ mValidationIndex++;
+ }
+
+ /**
+ * Returns the enum TransportType.
+ *
+ * <p>This method only supports a limited set of common transport type combinations that can be
+ * measured through metrics, and will return {@link TransportType#TT_UNKNOWN} for others. This
+ * ensures that, for example, metrics for a TRANSPORT_NEW_UNKNOWN | TRANSPORT_ETHERNET network
+ * cannot get aggregated with / compared with a "normal" TRANSPORT_ETHERNET network without
+ * noticing.
+ *
+ * @param nc Capabilities to extract transport type from.
+ * @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;
+
+ final int trCount = nc.getTransportTypes().length;
+ 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);
+
+ // VPN networks are not subject to validation and should not see validation stats, but
+ // metrics could be added to measure private DNS probes only.
+ if (trCount == 3 && hasCellular && hasWifi && hasVpn) {
+ return TransportType.TT_WIFI_CELLULAR_VPN;
+ }
+
+ if (trCount == 2 && hasVpn) {
+ if (hasWifi) return TransportType.TT_WIFI_VPN;
+ if (hasCellular) return TransportType.TT_CELLULAR_VPN;
+ if (hasBT) return TransportType.TT_BLUETOOTH_VPN;
+ if (hasEthernet) return TransportType.TT_ETHERNET_VPN;
+ }
+
+ if (trCount == 1) {
+ if (hasWifi) return TransportType.TT_WIFI;
+ if (hasCellular) return TransportType.TT_CELLULAR;
+ if (hasBT) return TransportType.TT_BLUETOOTH;
+ if (hasEthernet) return TransportType.TT_ETHERNET;
+ if (hasWifiAware) return TransportType.TT_WIFI_AWARE;
+ if (hasLopan) return TransportType.TT_LOWPAN;
+ // TODO: consider having a TT_VPN for VPN-only transport
+ }
+
+ 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) {
+ // TODO: consider adding a VR_PARTIAL_SUCCESS field to track cases where users accepted
+ // partial connectivity
+ 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;
+ }
+ }
+
+ /**
+ * Add a network probe event to the metrics builder.
+ */
+ public void addProbeEvent(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.
+ // TODO: consider recording the total number of probes in a separate field to know how
+ // many probes are skipped.
+ 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))
+ // TODO: rename this field to setRemainingKBytes, or use a long
+ .setRemainingBytes(
+ NetworkStackUtils.saturatedCast(capportData.getByteLimit() / 1000))
+ .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.
+ *
+ * <p>This is a no-op if {@link #startCollection(NetworkCapabilities)} was not called since the
+ * last call to this method.
+ */
+ public NetworkValidationReported maybeStopCollectionAndSend() {
+ 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 stats = mStatsBuilder.build();
+ final byte[] probeEvents = stats.getProbeEvents().toByteArray();
+
+ NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_VALIDATION_REPORTED,
+ stats.getTransportType().getNumber(),
+ probeEvents,
+ stats.getValidationResult().getNumber(),
+ stats.getLatencyMicros(),
+ stats.getValidationIndex(),
+ stats.getRandomNumber());
+ mWatch.reset();
+ return stats;
+ }
+}
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index b6b1fff..40de26e 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -129,6 +129,8 @@
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.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.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;
@@ -446,7 +450,14 @@
protected boolean mIsCaptivePortalCheckEnabled;
private boolean mUseHttps;
- // The total number of captive portal detection attempts for this NetworkMonitor instance.
+ /**
+ * The total number of completed validation attempts (network validated or a captive portal was
+ * detected) for this NetworkMonitor instance.
+ * This does not include attempts that were interrupted, retried or finished with a result that
+ * is not success or portal. See {@code mValidationIndex} in {@link NetworkValidationMetrics}
+ * for a count of all attempts.
+ * TODO: remove when removing legacy metrics.
+ */
private int mValidations = 0;
// Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
@@ -500,6 +511,18 @@
private final boolean mPrivateIpNoInternetEnabled;
+ private final boolean mMetricsEnabled;
+
+ // The validation metrics are accessed by individual probe threads, and by the StateMachine
+ // thread. All accesses must be synchronized to make sure the StateMachine thread can see
+ // reports from all probes.
+ // TODO: as that most usage is in the StateMachine thread and probes only add their probe
+ // events, consider having probes return their stats to the StateMachine, and only access this
+ // member on the StateMachine thread without synchronization.
+ @GuardedBy("mNetworkValidationMetrics")
+ private final NetworkValidationMetrics mNetworkValidationMetrics =
+ new NetworkValidationMetrics();
+
private int getCallbackVersion(INetworkMonitorCallbacks cb) {
int version;
try {
@@ -563,6 +586,8 @@
mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled();
+ mMetricsEnabled = deps.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.VALIDATION_METRICS_VERSION, true /* defaultEnabled */);
mUseHttps = getUseHttpsValidation();
mCaptivePortalUserAgent = getCaptivePortalUserAgent();
mCaptivePortalHttpsUrls = makeCaptivePortalHttpsUrls();
@@ -775,6 +800,51 @@
}
}
+ private void startMetricsCollection() {
+ if (!mMetricsEnabled) return;
+ try {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.startCollection(mNetworkCapabilities);
+ }
+ } catch (Exception e) {
+ Log.wtf(TAG, "Error resetting validation metrics", e);
+ }
+ }
+
+ private void recordProbeEventMetrics(ProbeType type, long latencyMicros, ProbeResult result,
+ CaptivePortalDataShim capportData) {
+ if (!mMetricsEnabled) return;
+ try {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.addProbeEvent(type, latencyMicros, result, capportData);
+ }
+ } catch (Exception e) {
+ Log.wtf(TAG, "Error recording probe event", e);
+ }
+ }
+
+ private void recordValidationResult(int result, String redirectUrl) {
+ if (!mMetricsEnabled) return;
+ try {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.setValidationResult(result, redirectUrl);
+ }
+ } catch (Exception e) {
+ Log.wtf(TAG, "Error recording validation result", e);
+ }
+ }
+
+ private void maybeStopCollectionAndSendMetrics() {
+ if (!mMetricsEnabled) return;
+ try {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.maybeStopCollectionAndSend();
+ }
+ } catch (Exception e) {
+ Log.wtf(TAG, "Error sending validation stats", e);
+ }
+ }
+
// 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 +857,7 @@
transitionTo(mEvaluatingState);
return HANDLED;
case CMD_NETWORK_DISCONNECTED:
+ maybeStopCollectionAndSendMetrics();
logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
quit();
return HANDLED;
@@ -938,6 +1009,7 @@
initSocketTrackingIfRequired();
// start periodical polling.
sendTcpPollingEvent();
+ maybeStopCollectionAndSendMetrics();
}
private void initSocketTrackingIfRequired() {
@@ -957,6 +1029,9 @@
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 +1360,7 @@
sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
mValidations++;
+ maybeStopCollectionAndSendMetrics();
}
@Override
@@ -1316,6 +1392,12 @@
notifyPrivateDnsConfigResolved();
} else {
handlePrivateDnsEvaluationFailure();
+ // The private DNS probe fails-fast if the server hostname cannot
+ // be resolved. Record it as a failure with zero latency.
+ // TODO: refactor this together with the probe recorded in
+ // sendPrivateDnsProbe, so logging is symmetric / easier to follow.
+ recordProbeEventMetrics(ProbeType.PT_PRIVDNS, 0 /* latency */,
+ ProbeResult.PR_FAILURE, null /* capportData */);
break;
}
}
@@ -1425,6 +1507,8 @@
validationLog(PROBE_PRIVDNS, host,
String.format("%dus - Error: %s", time, uhe.getMessage()));
}
+ recordProbeEventMetrics(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 +1519,14 @@
@Override
public void enter() {
+ // When starting a full probe cycle here, record any pending stats (for example if
+ // CMD_FORCE_REEVALUATE was called before evaluation finished, as can happen in
+ // EvaluatingPrivateDnsState).
+ maybeStopCollectionAndSendMetrics();
+ // Restart the metrics collection timers. Metrics will be stopped and sent when the
+ // validation attempt finishes (as success, failure or portal), or if it is interrupted
+ // (by being restarted or if NetworkMonitor stops).
+ startMetricsCollection();
if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
//Don't continue to blame UID forever.
TrafficStats.clearThreadStatsUid();
@@ -1516,6 +1608,9 @@
private class WaitingForNextProbeState extends State {
@Override
public void enter() {
+ // Send metrics for this evaluation attempt. Metrics collection (and its timers) will be
+ // restarted when the next probe starts.
+ maybeStopCollectionAndSendMetrics();
scheduleNextProbe();
}
@@ -2265,6 +2360,8 @@
// network validation (the HTTPS probe, which would likely fail anyway) or the PAC probe.
if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
&& (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
+ recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
+ 0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
return CaptivePortalProbeResult.PRIVATE_IP;
}
return sendHttpProbe(url, probeType, null);
@@ -2296,6 +2393,9 @@
result = ValidationProbeEvent.DNS_FAILURE;
}
final long latency = watch.stop();
+ recordProbeEventMetrics(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 +2513,17 @@
}
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);
}
+ recordProbeEventMetrics(NetworkValidationMetrics.probeTypeToEnum(probeType),
+ probeTimer.stop(), NetworkValidationMetrics.httpProbeResultToEnum(probeResult),
+ null /* capportData */);
+ return probeResult;
}
@VisibleForTesting
@@ -2570,8 +2675,9 @@
super(deps, proxy, url, captivePortalApiUrl);
}
- private CaptivePortalDataShim tryCapportApiProbe() {
- if (mCaptivePortalApiUrl == null) return null;
+ private CaptivePortalDataShim sendCapportApiProbe() {
+ // TODO: consider adding metrics counters for each case returning null in this method
+ // (cases where the API is not implemented properly).
validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl);
final String apiContent;
@@ -2610,26 +2716,38 @@
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();
+ recordProbeEventMetrics(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 +3461,7 @@
p.redirectUrl = redirectUrl;
p.timestampMillis = SystemClock.elapsedRealtime();
notifyNetworkTested(p);
+ recordValidationResult(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..98e7b63
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/metrics/NetworkValidationMetricsTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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 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_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.startCollection(WIFI_CAPABILITIES);
+ // 1. PT_DNS probe
+ metrics.addProbeEvent(ProbeType.PT_DNS, 1234, ProbeResult.PR_SUCCESS, null);
+ // 2. Consecutive PT_HTTP probe failure
+ for (int i = 0; i < 30; i++) {
+ metrics.addProbeEvent(ProbeType.PT_HTTP, 1234, ProbeResult.PR_FAILURE, null);
+ }
+
+ // Write metric into statsd
+ final NetworkValidationReported stats = metrics.maybeStopCollectionAndSend();
+
+ // 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 {
+ final long bytesRemaining = 12_345L;
+ 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 = Integer.MAX_VALUE + 12344567L;
+ metrics.startCollection(WIFI_CAPABILITIES);
+
+ final JSONObject info = new JSONObject(apiContent);
+ final CaptivePortalDataShim captivePortalData = CaptivePortalDataShimImpl.isSupported()
+ ? CaptivePortalDataShimImpl.fromJson(info) : null;
+
+ // 1. PT_CAPPORT_API probe w CapportApiData info
+ metrics.addProbeEvent(ProbeType.PT_CAPPORT_API, 1234, ProbeResult.PR_SUCCESS,
+ captivePortalData);
+ // 2. PT_CAPPORT_API probe w/o CapportApiData info
+ metrics.addProbeEvent(ProbeType.PT_CAPPORT_API, 1234, ProbeResult.PR_FAILURE, null);
+
+ // 3. PT_DNS probe
+ metrics.addProbeEvent(ProbeType.PT_DNS, 5678, ProbeResult.PR_FAILURE, null);
+
+ // 4. PT_HTTP probe
+ metrics.addProbeEvent(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.maybeStopCollectionAndSend();
+
+ // 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());
+ if (CaptivePortalDataShimImpl.isSupported()) {
+ assertTrue(probeEvent.hasCapportApiData());
+ // 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() / 1000, 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());
+ }
+}