Merge "Add createSetLinkStateMessage method at RtNetlinkLinkMessage" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index bcf5e8b..94adc5b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -414,6 +414,15 @@
"exclude-annotation": "androidx.test.filters.RequiresDevice"
}
]
+ },
+ // TODO: upgrade to presubmit. Postsubmit on virtual devices to monitor flakiness only.
+ {
+ "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
}
],
"imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index d04660d..70b38a4 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -32,16 +32,19 @@
java_defaults {
name: "TetheringExternalLibs",
+ defaults: [
+ "TetheringApiLevel",
+ ],
// Libraries not including Tethering's own framework-tethering (different flavors of that one
// are needed depending on the build rule)
libs: [
"connectivity-internal-api-util",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-connectivity-t.stubs.module_lib",
"framework-statsd.stubs.module_lib",
- "framework-wifi",
- "framework-bluetooth",
+ "framework-wifi.stubs.module_lib",
+ "framework-bluetooth.stubs.module_lib",
"unsupportedappusage",
],
defaults_visibility: ["//visibility:private"],
@@ -54,6 +57,7 @@
"src/**/*.java",
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
+ ":statslog-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
@@ -89,7 +93,6 @@
defaults: [
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -105,7 +108,6 @@
name: "TetheringApiStableLib",
defaults: [
"TetheringAndroidLibraryDefaults",
- "TetheringApiLevel",
"TetheringReleaseTargetSdk",
],
static_libs: [
@@ -194,7 +196,6 @@
name: "Tethering",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
@@ -208,7 +209,6 @@
name: "TetheringNext",
defaults: [
"TetheringAppDefaults",
- "TetheringApiLevel",
"ConnectivityNextEnableDefaults",
],
static_libs: ["TetheringApiCurrentLib"],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8d96066..0c05354 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -98,7 +98,6 @@
],
canned_fs_config: "canned_fs_config",
bpfs: [
- "block.o",
"clatd.o",
"dscpPolicy.o",
"netd.o",
@@ -114,7 +113,7 @@
"current_sdkinfo",
"netbpfload.33rc",
"netbpfload.35rc",
- "ot-daemon.init.34rc",
+ "ot-daemon.34rc",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 4d1e7ef..e6e99f4 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -359,6 +359,7 @@
} catch (IllegalStateException e) {
// Silent if the rule already exists. Note that the errno EEXIST was rethrown as
// IllegalStateException. See BpfMap#insertEntry.
+ return false;
}
return true;
}
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index d28a397..026b1c3 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -140,6 +140,8 @@
/**
* Adds a tethering IPv4 offload rule to appropriate BPF map.
+ *
+ * @return true iff the map was modified, false if the key already exists or there was an error.
*/
public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
@NonNull Tether4Value value);
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e00756..2f3307a 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -29,6 +29,7 @@
"//packages/modules/Connectivity/framework-t",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
// Using for test only
"//cts/tests/netlegacy22.api",
@@ -46,6 +47,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
diff --git a/Tethering/res/values-fa/strings.xml b/Tethering/res/values-fa/strings.xml
index d7f2543..fdfd5c4 100644
--- a/Tethering/res/values-fa/strings.xml
+++ b/Tethering/res/values-fa/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="tethered_notification_title" msgid="5350162111436634622">"اشتراکگذاری اینترنت یا نقطه اتصال فعال است"</string>
- <string name="tethered_notification_message" msgid="2338023450330652098">"برای راهاندازی، ضربه بزنید."</string>
+ <string name="tethered_notification_message" msgid="2338023450330652098">"برای راهاندازی، تکضرب بزنید."</string>
<string name="disable_tether_notification_title" msgid="3183576627492925522">"اشتراکگذاری اینترنت غیرفعال است"</string>
<string name="disable_tether_notification_message" msgid="6655882039707534929">"برای جزئیات، با سرپرستتان تماس بگیرید"</string>
<string name="notification_channel_tethering_status" msgid="7030733422705019001">"وضعیت نقطه اتصال و اشتراکگذاری اینترنت"</string>
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 5c853f4..89e06da 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -33,10 +33,12 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.TetheringUtils.getTetheringJniLibraryName;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.IpPrefix;
import android.net.LinkProperties;
@@ -65,6 +67,7 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
@@ -84,6 +87,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
+import com.android.server.ConnectivityStatsLog;
import java.io.IOException;
import java.net.Inet4Address;
@@ -148,6 +152,13 @@
@VisibleForTesting
static final int CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS = 60_000;
+ // The interval is set to 5 minutes to strike a balance between minimizing
+ // the amount of metrics data uploaded and providing sufficient resolution
+ // to track changes in forwarding rules. This choice considers the minimum
+ // push metrics sampling interval of 5 minutes and the 3-minute timeout
+ // for forwarding rules.
+ @VisibleForTesting
+ static final int CONNTRACK_METRICS_UPDATE_INTERVAL_MS = 300_000;
@VisibleForTesting
static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432_000;
@VisibleForTesting
@@ -314,12 +325,23 @@
scheduleConntrackTimeoutUpdate();
};
+ private final boolean mSupportActiveSessionsMetrics;
+
+ // Runnable that used by scheduling next refreshing of conntrack metrics sampling.
+ private final Runnable mScheduledConntrackMetricsSampling = () -> {
+ uploadConntrackMetricsSample();
+ scheduleConntrackMetricsSampling();
+ };
+
// TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@VisibleForTesting
public abstract static class Dependencies {
/** Get handler. */
@NonNull public abstract Handler getHandler();
+ /** Get context. */
+ @NonNull public abstract Context getContext();
+
/** Get netd. */
@NonNull public abstract INetd getNetd();
@@ -472,6 +494,19 @@
return null;
}
}
+
+ /** Send a TetheringActiveSessionsReported event. */
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ ConnectivityStatsLog.write(ConnectivityStatsLog.TETHERING_ACTIVE_SESSIONS_REPORTED,
+ lastMaxSessionCount);
+ }
+
+ /**
+ * @see DeviceConfigUtils#isTetheringFeatureEnabled
+ */
+ public boolean isFeatureEnabled(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+ }
}
@VisibleForTesting
@@ -508,32 +543,57 @@
if (!mBpfCoordinatorShim.isInitialized()) {
mLog.e("Bpf shim not initialized");
}
+
+ // BPF IPv4 forwarding only supports on S+.
+ mSupportActiveSessionsMetrics = mDeps.isAtLeastS()
+ && mDeps.isFeatureEnabled(mDeps.getContext(), TETHER_ACTIVE_SESSIONS_METRICS);
}
/**
- * Start BPF tethering offload stats and conntrack timeout polling.
+ * Start BPF tethering offload stats and conntrack polling.
* Note that this can be only called on handler thread.
*/
- private void startStatsAndConntrackTimeoutPolling() {
+ private void startStatsAndConntrackPolling() {
schedulePollingStats();
scheduleConntrackTimeoutUpdate();
+ if (mSupportActiveSessionsMetrics) {
+ scheduleConntrackMetricsSampling();
+ }
mLog.i("Polling started.");
}
/**
- * Stop BPF tethering offload stats and conntrack timeout polling.
+ * Stop BPF tethering offload stats and conntrack polling.
* The data limit cleanup and the tether stats maps cleanup are not implemented here.
* These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
* last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
* functionality.
* Note that this can be only called on handler thread.
*/
- private void stopStatsAndConntrackTimeoutPolling() {
+ private void stopStatsAndConntrackPolling() {
// Stop scheduled polling conntrack timeout.
if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
}
+ // Stop scheduled polling conntrack metrics sampling and
+ // clear counters in case there is any counter unsync problem
+ // previously due to possible bpf failures.
+ // Normally this won't happen because all clients are cleared before
+ // reaching here. See IpServer.BaseServingState#exit().
+ if (mSupportActiveSessionsMetrics) {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+ final int currentCount = mBpfConntrackEventConsumer.getCurrentConnectionCount();
+ if (currentCount != 0) {
+ Log.wtf(TAG, "Unexpected CurrentConnectionCount: " + currentCount);
+ }
+ // Avoid sending metrics when tethering is about to close.
+ // This leads to a missing final sample before disconnect
+ // but avoids possibly duplicating the last metric in the upload.
+ mBpfConntrackEventConsumer.clearConnectionCounters();
+ }
// Stop scheduled polling stats and poll the latest stats from BPF maps.
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
@@ -867,7 +927,7 @@
// Start monitoring and polling when the first IpServer is added.
if (mServedIpServers.isEmpty()) {
- startStatsAndConntrackTimeoutPolling();
+ startStatsAndConntrackPolling();
startConntrackMonitoring();
mIpNeighborMonitor.start();
mLog.i("Neighbor monitoring started.");
@@ -890,7 +950,7 @@
// Stop monitoring and polling when the last IpServer is removed.
if (mServedIpServers.isEmpty()) {
- stopStatsAndConntrackTimeoutPolling();
+ stopStatsAndConntrackPolling();
stopConntrackMonitoring();
mIpNeighborMonitor.stop();
mLog.i("Neighbor monitoring stopped.");
@@ -1031,6 +1091,10 @@
for (final Tether4Key k : deleteDownstreamRuleKeys) {
mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, k);
}
+ if (mSupportActiveSessionsMetrics) {
+ mBpfConntrackEventConsumer.decreaseCurrentConnectionCount(
+ deleteUpstreamRuleKeys.size());
+ }
// Cleanup each upstream interface by a set which avoids duplicated work on the same
// upstream interface. Cleaning up the same interface twice (or more) here may raise
@@ -1300,6 +1364,13 @@
pw.increaseIndent();
dumpCounters(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mSupportActiveSessionsMetrics: " + mSupportActiveSessionsMetrics);
+ pw.println("getLastMaxConnectionCount: "
+ + mBpfConntrackEventConsumer.getLastMaxConnectionCount());
+ pw.println("getCurrentConnectionCount: "
+ + mBpfConntrackEventConsumer.getCurrentConnectionCount());
}
private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -1991,6 +2062,21 @@
// while TCP status is established.
@VisibleForTesting
class BpfConntrackEventConsumer implements ConntrackEventConsumer {
+ /**
+ * Tracks the current number of tethering connections and the maximum
+ * observed since the last metrics collection. Used to provide insights
+ * into the distribution of active tethering sessions for metrics reporting.
+
+ * These variables are accessed on the handler thread, which includes:
+ * 1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
+ * 2. ConntrackEvents indicating the removal of a tethering client,
+ * triggering the removal of associated rules.
+ * 3. Removal of the last IpServer, which resets counters to handle
+ * potential synchronization issues.
+ */
+ private int mLastMaxConnectionCount = 0;
+ private int mCurrentConnectionCount = 0;
+
// The upstream4 and downstream4 rules are built as the following tables. Only raw ip
// upstream interface is supported. Note that the field "lastUsed" is only updated by
// BPF program which records the last used time for a given rule.
@@ -2124,6 +2210,10 @@
return;
}
+ if (mSupportActiveSessionsMetrics) {
+ decreaseCurrentConnectionCount(1);
+ }
+
maybeClearLimit(upstreamIndex);
return;
}
@@ -2136,8 +2226,50 @@
maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex);
maybeSetLimit(upstreamIndex);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
- mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
+
+ final boolean addedUpstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ UPSTREAM, upstream4Key, upstream4Value);
+ final boolean addedDownstream = mBpfCoordinatorShim.tetherOffloadRuleAdd(
+ DOWNSTREAM, downstream4Key, downstream4Value);
+ if (addedUpstream != addedDownstream) {
+ Log.wtf(TAG, "The bidirectional rules should be added concurrently ("
+ + "upstream: " + addedUpstream
+ + ", downstream: " + addedDownstream + ")");
+ return;
+ }
+ if (mSupportActiveSessionsMetrics && addedUpstream && addedDownstream) {
+ mCurrentConnectionCount++;
+ mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
+ mLastMaxConnectionCount);
+ }
+ }
+
+ public int getLastMaxConnectionAndResetToCurrent() {
+ final int ret = mLastMaxConnectionCount;
+ mLastMaxConnectionCount = mCurrentConnectionCount;
+ return ret;
+ }
+
+ /** For dumping current state only. */
+ public int getLastMaxConnectionCount() {
+ return mLastMaxConnectionCount;
+ }
+
+ public int getCurrentConnectionCount() {
+ return mCurrentConnectionCount;
+ }
+
+ public void decreaseCurrentConnectionCount(int count) {
+ mCurrentConnectionCount -= count;
+ if (mCurrentConnectionCount < 0) {
+ Log.wtf(TAG, "Unexpected mCurrentConnectionCount: "
+ + mCurrentConnectionCount);
+ }
+ }
+
+ public void clearConnectionCounters() {
+ mCurrentConnectionCount = 0;
+ mLastMaxConnectionCount = 0;
}
}
@@ -2477,6 +2609,11 @@
});
}
+ private void uploadConntrackMetricsSample() {
+ mDeps.sendTetheringActiveSessionsReported(
+ mBpfConntrackEventConsumer.getLastMaxConnectionAndResetToCurrent());
+ }
+
private void schedulePollingStats() {
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
@@ -2494,6 +2631,15 @@
CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
}
+ private void scheduleConntrackMetricsSampling() {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+
+ mHandler.postDelayed(mScheduledConntrackMetricsSampling,
+ CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ }
+
// Return IPv6 downstream forwarding rule map. This is used for testing only.
// Note that this can be only called on handler thread.
@NonNull
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d62f18f..1938a08 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -374,6 +374,11 @@
}
@NonNull
+ public Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -2082,6 +2087,7 @@
chooseUpstreamType(true);
mTryCell = false;
}
+ mTetheringMetrics.initUpstreamUsageBaseline();
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 298940e..c9817c9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -144,6 +144,12 @@
/** A flag for using synchronous or asynchronous state machine. */
public static boolean USE_SYNC_SM = false;
+ /**
+ * A feature flag to control whether the active sessions metrics should be enabled.
+ * Disabled by default.
+ */
+ public static final String TETHER_ACTIVE_SESSIONS_METRICS = "tether_active_sessions_metrics";
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 2202106..fc50faf 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -16,6 +16,8 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -24,6 +26,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.UID_TETHERING;
import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
import static android.net.NetworkTemplate.MATCH_ETHERNET;
import static android.net.NetworkTemplate.MATCH_MOBILE;
@@ -52,13 +55,19 @@
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
import android.annotation.Nullable;
+import android.app.usage.NetworkStats;
+import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.NetworkCapabilities;
import android.net.NetworkTemplate;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
@@ -75,6 +84,10 @@
/**
* Collection of utilities for tethering metrics.
*
+ * <p>This class is thread-safe. All accesses to this class will be either posting to the internal
+ * handler thread for processing or checking whether the access is from the internal handler
+ * thread. However, the constructor is an exception, as it is called on another thread.
+ *
* To see if the logs are properly sent to statsd, execute following commands
*
* $ adb shell cmd stats print-logs
@@ -93,11 +106,16 @@
*/
private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
"tether_upstream_data_usage_metrics";
+ @VisibleForTesting
+ static final DataUsage EMPTY = new DataUsage(0L /* txBytes */, 0L /* rxBytes */);
private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+ private final ArrayMap<UpstreamType, DataUsage> mUpstreamUsageBaseline = new ArrayMap<>();
private final Context mContext;
private final Dependencies mDependencies;
+ private final NetworkStatsManager mNetworkStatsManager;
+ private final Handler mHandler;
private UpstreamType mCurrentUpstream = null;
private Long mCurrentUpStreamStartTime = 0L;
@@ -136,6 +154,14 @@
return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
}
+
+ /**
+ * @see Handler
+ */
+ @NonNull
+ public Handler createHandler(Looper looper) {
+ return new Handler(looper);
+ }
}
/**
@@ -150,24 +176,49 @@
TetheringMetrics(Context context, Dependencies dependencies) {
mContext = context;
mDependencies = dependencies;
+ mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+ final HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ mHandler = dependencies.createHandler(thread.getLooper());
}
- private static class DataUsage {
- final long mTxBytes;
- final long mRxBytes;
+ @VisibleForTesting
+ static class DataUsage {
+ public final long txBytes;
+ public final long rxBytes;
DataUsage(long txBytes, long rxBytes) {
- mTxBytes = txBytes;
- mRxBytes = rxBytes;
+ this.txBytes = txBytes;
+ this.rxBytes = rxBytes;
}
- public long getTxBytes() {
- return mTxBytes;
+ /*** Calculate the data usage delta from give new and old usage */
+ public static DataUsage subtract(DataUsage newUsage, DataUsage oldUsage) {
+ return new DataUsage(
+ newUsage.txBytes - oldUsage.txBytes,
+ newUsage.rxBytes - oldUsage.rxBytes);
}
- public long getRxBytes() {
- return mRxBytes;
+ @Override
+ public int hashCode() {
+ return (int) (txBytes & 0xFFFFFFFF)
+ + ((int) (txBytes >> 32) * 3)
+ + ((int) (rxBytes & 0xFFFFFFFF) * 5)
+ + ((int) (rxBytes >> 32) * 7);
}
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof DataUsage)) {
+ return false;
+ }
+ return txBytes == ((DataUsage) other).txBytes
+ && rxBytes == ((DataUsage) other).rxBytes;
+ }
+
}
private static class RecordUpstreamEvent {
@@ -194,6 +245,10 @@
* @param callerPkg The package name of the caller.
*/
public void createBuilder(final int downstreamType, final String callerPkg) {
+ mHandler.post(() -> handleCreateBuilder(downstreamType, callerPkg));
+ }
+
+ private void handleCreateBuilder(final int downstreamType, final String callerPkg) {
NetworkTetheringReported.Builder statsBuilder = NetworkTetheringReported.newBuilder()
.setDownstreamType(downstreamTypeToEnum(downstreamType))
.setUserType(userTypeToEnum(callerPkg))
@@ -211,6 +266,10 @@
* @param errCode The error code to set.
*/
public void updateErrorCode(final int downstreamType, final int errCode) {
+ mHandler.post(() -> handleUpdateErrorCode(downstreamType, errCode));
+ }
+
+ private void handleUpdateErrorCode(final int downstreamType, final int errCode) {
NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -219,13 +278,26 @@
statsBuilder.setErrorCode(errorCodeToEnum(errCode));
}
- private DataUsage calculateDataUsage(@Nullable UpstreamType upstream) {
+ /**
+ * Calculates the data usage difference between the current and previous usage for the
+ * specified upstream type.
+ *
+ * @return A DataUsage object containing the calculated difference in transmitted (tx) and
+ * received (rx) bytes.
+ */
+ private DataUsage calculateDataUsageDelta(@Nullable UpstreamType upstream) {
if (upstream != null && mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
&& isUsageSupportedForUpstreamType(upstream)) {
- // TODO: Implement data usage calculation for the upstream type.
- return new DataUsage(0L, 0L);
+ final DataUsage oldUsage = mUpstreamUsageBaseline.getOrDefault(upstream, EMPTY);
+ if (oldUsage.equals(EMPTY)) {
+ Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+ return EMPTY;
+ }
+ // TODO(b/352537247): Fix data usage which might be incorrect if the device uses
+ // tethering with the same upstream for over 15 days.
+ return DataUsage.subtract(getCurrentDataUsageForUpstreamType(upstream), oldUsage);
}
- return new DataUsage(0L, 0L);
+ return EMPTY;
}
/**
@@ -234,12 +306,16 @@
* @param ns The UpstreamNetworkState object representing the current upstream network state.
*/
public void maybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
+ mHandler.post(() -> handleMaybeUpdateUpstreamType(ns));
+ }
+
+ private void handleMaybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
if (upstream.equals(mCurrentUpstream)) return;
final long newTime = mDependencies.timeNow();
if (mCurrentUpstream != null) {
- final DataUsage dataUsage = calculateDataUsage(upstream);
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
mCurrentUpstream, dataUsage));
}
@@ -292,14 +368,14 @@
final long startTime = Math.max(downstreamStartTime, event.mStartTime);
// Handle completed upstream events.
addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
- event.mUpstreamType, event.mDataUsage.mTxBytes, event.mDataUsage.mRxBytes);
+ event.mUpstreamType, event.mDataUsage.txBytes, event.mDataUsage.rxBytes);
}
final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
final long stopTime = mDependencies.timeNow();
// Handle the last upstream event.
- final DataUsage dataUsage = calculateDataUsage(mCurrentUpstream);
+ final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
- dataUsage.mTxBytes, dataUsage.mRxBytes);
+ dataUsage.txBytes, dataUsage.rxBytes);
statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
}
@@ -315,6 +391,10 @@
* @param downstreamType the type of downstream event to remove statistics for
*/
public void sendReport(final int downstreamType) {
+ mHandler.post(() -> handleSendReport(downstreamType));
+ }
+
+ private void handleSendReport(final int downstreamType) {
final NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
if (statsBuilder == null) {
Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -335,8 +415,7 @@
*
* @param reported a NetworkTetheringReported object containing statistics to write
*/
- @VisibleForTesting
- public void write(@NonNull final NetworkTetheringReported reported) {
+ private void write(@NonNull final NetworkTetheringReported reported) {
final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
mDependencies.write(reported);
if (DBG) {
@@ -358,12 +437,67 @@
}
/**
+ * Initialize the upstream data usage baseline when tethering is turned on.
+ */
+ public void initUpstreamUsageBaseline() {
+ mHandler.post(() -> handleInitUpstreamUsageBaseline());
+ }
+
+ private void handleInitUpstreamUsageBaseline() {
+ if (!(mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
+ && mUpstreamUsageBaseline.isEmpty())) {
+ return;
+ }
+
+ for (UpstreamType type : UpstreamType.values()) {
+ if (!isUsageSupportedForUpstreamType(type)) continue;
+ mUpstreamUsageBaseline.put(type, getCurrentDataUsageForUpstreamType(type));
+ }
+ }
+
+ @VisibleForTesting
+ @NonNull
+ DataUsage getDataUsageFromUpstreamType(@NonNull UpstreamType type) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ return mUpstreamUsageBaseline.getOrDefault(type, EMPTY);
+ }
+
+
+ /**
+ * Get the current usage for given upstream type.
+ */
+ @NonNull
+ private DataUsage getCurrentDataUsageForUpstreamType(@NonNull UpstreamType type) {
+ final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState(
+ buildNetworkTemplateForUpstreamType(type), Long.MIN_VALUE, Long.MAX_VALUE,
+ UID_TETHERING, TAG_NONE, STATE_ALL);
+
+ final NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+ Long totalTxBytes = 0L;
+ Long totalRxBytes = 0L;
+ while (stats.hasNextBucket()) {
+ stats.getNextBucket(bucket);
+ totalTxBytes += bucket.getTxBytes();
+ totalRxBytes += bucket.getRxBytes();
+ }
+ return new DataUsage(totalTxBytes, totalRxBytes);
+ }
+
+ /**
* Cleans up the variables related to upstream events when tethering is turned off.
*/
public void cleanup() {
+ mHandler.post(() -> handleCleanup());
+ }
+
+ private void handleCleanup() {
mUpstreamEventList.clear();
mCurrentUpstream = null;
mCurrentUpStreamStartTime = 0L;
+ mUpstreamUsageBaseline.clear();
}
private DownstreamType downstreamTypeToEnum(final int ifaceType) {
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 337d408..2211546 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -38,9 +38,9 @@
"connectivity-net-module-utils-bpf",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 1eb6255..423b9b8 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -568,6 +568,12 @@
return nif.getMTU();
}
+ protected int getIndexByName(String ifaceName) throws SocketException {
+ NetworkInterface nif = NetworkInterface.getByName(ifaceName);
+ assertNotNull("Can't get NetworkInterface object for " + ifaceName, nif);
+ return nif.getIndex();
+ }
+
protected TapPacketReader makePacketReader(final TestNetworkInterface iface) throws Exception {
FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
return makePacketReader(fd, getMTU(iface));
@@ -968,6 +974,11 @@
return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
}
+ protected String getUpstreamInterfaceName() {
+ if (mUpstreamTracker == null) return null;
+ return mUpstreamTracker.getTestIface().getInterfaceName();
+ }
+
protected <T> List<T> toList(T... array) {
return Arrays.asList(array);
}
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 049f5f0..32b2f3e 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -1066,24 +1066,34 @@
runUdp4Test();
}
- private ClatEgress4Value getClatEgress4Value() throws Exception {
+ private ClatEgress4Value getClatEgress4Value(int clatIfaceIndex) throws Exception {
// Command: dumpsys connectivity clatEgress4RawBpfMap
final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG};
final HashMap<ClatEgress4Key, ClatEgress4Value> egress4Map = pollRawMapFromDump(
ClatEgress4Key.class, ClatEgress4Value.class, Context.CONNECTIVITY_SERVICE, args);
assertNotNull(egress4Map);
- assertEquals(1, egress4Map.size());
- return egress4Map.entrySet().iterator().next().getValue();
+ for (Map.Entry<ClatEgress4Key, ClatEgress4Value> entry : egress4Map.entrySet()) {
+ ClatEgress4Key key = entry.getKey();
+ if (key.iif == clatIfaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
}
- private ClatIngress6Value getClatIngress6Value() throws Exception {
+ private ClatIngress6Value getClatIngress6Value(int ifaceIndex) throws Exception {
// Command: dumpsys connectivity clatIngress6RawBpfMap
final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG};
final HashMap<ClatIngress6Key, ClatIngress6Value> ingress6Map = pollRawMapFromDump(
ClatIngress6Key.class, ClatIngress6Value.class, Context.CONNECTIVITY_SERVICE, args);
assertNotNull(ingress6Map);
- assertEquals(1, ingress6Map.size());
- return ingress6Map.entrySet().iterator().next().getValue();
+ for (Map.Entry<ClatIngress6Key, ClatIngress6Value> entry : ingress6Map.entrySet()) {
+ ClatIngress6Key key = entry.getKey();
+ if (key.iif == ifaceIndex) {
+ return entry.getValue();
+ }
+ }
+ return null;
}
/**
@@ -1115,8 +1125,13 @@
final Inet6Address clatIp6 = getClatIpv6Address(tester, tethered);
// Get current values before sending packets.
- final ClatEgress4Value oldEgress4 = getClatEgress4Value();
- final ClatIngress6Value oldIngress6 = getClatIngress6Value();
+ final String ifaceName = getUpstreamInterfaceName();
+ final int ifaceIndex = getIndexByName(ifaceName);
+ final int clatIfaceIndex = getIndexByName("v4-" + ifaceName);
+ final ClatEgress4Value oldEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value oldIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(oldEgress4);
+ assertNotNull(oldIngress6);
// Send an IPv4 UDP packet in original direction.
// IPv4 packet -- CLAT translation --> IPv6 packet
@@ -1145,8 +1160,10 @@
ByteBuffer.wrap(payload), l2mtu);
// After sending test packets, get stats again to verify their differences.
- final ClatEgress4Value newEgress4 = getClatEgress4Value();
- final ClatIngress6Value newIngress6 = getClatIngress6Value();
+ final ClatEgress4Value newEgress4 = getClatEgress4Value(clatIfaceIndex);
+ final ClatIngress6Value newIngress6 = getClatIngress6Value(ifaceIndex);
+ assertNotNull(newEgress4);
+ assertNotNull(newIngress6);
assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index c4d5636..1f1929c 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -26,7 +26,7 @@
target_sdk_version: "33",
libs: [
- "android.test.base",
+ "android.test.base.stubs",
],
srcs: [
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 24407ca..d0d23ac 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -62,9 +62,9 @@
// remove framework-minus-apex, ext, and framework-res
sdk_version: "core_platform",
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ext",
"framework-minus-apex",
"framework-res",
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index e54a7e0..5d22977 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -48,6 +48,7 @@
import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
+import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_METRICS_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -60,6 +61,7 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
import static com.android.testutils.MiscAsserts.assertSameElements;
import static org.junit.Assert.assertArrayEquals;
@@ -87,6 +89,7 @@
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
+import android.content.Context;
import android.net.INetd;
import android.net.InetAddresses;
import android.net.IpPrefix;
@@ -140,6 +143,8 @@
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestBpfMap;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
import org.junit.Before;
import org.junit.Rule;
@@ -171,6 +176,16 @@
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+ // This will set feature flags from @FeatureFlag annotations
+ // into the map before setUp() runs.
+ @Rule
+ public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+ new SetFeatureFlagsRule((name, enabled) -> {
+ mFeatureFlags.put(name, enabled);
+ return null;
+ }, (name) -> mFeatureFlags.getOrDefault(name, false));
+
private static final boolean IPV4 = true;
private static final boolean IPV6 = false;
@@ -406,6 +421,11 @@
return this;
}
+ public Builder setPrivateAddress(Inet4Address privateAddr) {
+ mPrivateAddr = privateAddr;
+ return this;
+ }
+
public Builder setRemotePort(int remotePort) {
mRemotePort = (short) remotePort;
return this;
@@ -429,6 +449,7 @@
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
+ @Mock private Context mMockContext;
@Mock private IpServer mIpServer;
@Mock private IpServer mIpServer2;
@Mock private TetheringConfiguration mTetherConfig;
@@ -475,6 +496,11 @@
}
@NonNull
+ public Context getContext() {
+ return mMockContext;
+ }
+
+ @NonNull
public INetd getNetd() {
return mNetd;
}
@@ -546,6 +572,16 @@
public IBpfMap<S32, S32> getBpfErrorMap() {
return mBpfErrorMap;
}
+
+ @Override
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ // No-op.
+ }
+
+ @Override
+ public boolean isFeatureEnabled(Context context, String name) {
+ return mFeatureFlags.getOrDefault(name, false);
+ }
});
@Before public void setUp() {
@@ -1977,6 +2013,229 @@
verify(mBpfDevMap, never()).updateEntry(any(), any());
}
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testMaxConnectionCount_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testMaxConnectionCount_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount(false);
+ }
+
+ private void doTestMaxConnectionCount(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+
+ // Prepare add/delete rule events.
+ final ArrayList<ConntrackEvent> addRuleEvents = new ArrayList<>();
+ final ArrayList<ConntrackEvent> delRuleEvents = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ final ConntrackEvent addEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ addRuleEvents.add(addEvent);
+ final ConntrackEvent delEvent = new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build();
+ delRuleEvents.add(delEvent);
+ }
+
+ // Add rules, verify counter increases.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(addRuleEvents.get(i));
+ assertConsumerCountersEquals(supportActiveSessionsMetrics ? i + 1 : 0);
+ }
+
+ // Add the same events again should not increase the counter because
+ // all events are already exist.
+ for (final ConntrackEvent event : addRuleEvents) {
+ mConsumer.accept(event);
+ assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+ }
+
+ // Verify removing non-existent items won't change the counters.
+ for (int i = 5; i < 8; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+ }
+
+ // Verify remove the rules decrease the counter.
+ // Note the max counter returns the max, so it returns the count before deleting.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ assertEquals(supportActiveSessionsMetrics ? 4 - i : 0,
+ mConsumer.getCurrentConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
+ mConsumer.getLastMaxConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
+ mConsumer.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ // Verify remove these rules again doesn't decrease the counter.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(delRuleEvents.get(i));
+ assertConsumerCountersEquals(0);
+ }
+ }
+
+ // Helper method to assert all counter values inside consumer.
+ private void assertConsumerCountersEquals(int expectedCount) {
+ assertEquals(expectedCount, mConsumer.getCurrentConnectionCount());
+ assertEquals(expectedCount, mConsumer.getLastMaxConnectionCount());
+ assertEquals(expectedCount, mConsumer.getLastMaxConnectionAndResetToCurrent());
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsEnabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void doTestMaxConnectionCount_removeClient_metricsDisabled() throws Exception {
+ doTestMaxConnectionCount_removeClient(false);
+ }
+
+ private void doTestMaxConnectionCount_removeClient(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+
+ // Add client information A and B on on the same downstream.
+ final ClientInfo clientA = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR, MAC_A);
+ final ClientInfo clientB = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR2, MAC_B);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientA);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientB);
+ assertClientInfoExists(mIpServer, clientA);
+ assertClientInfoExists(mIpServer, clientB);
+ assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+
+ // Add some rules for both clients.
+ final int addr1RuleCount = 5;
+ final int addr2RuleCount = 3;
+
+ for (int i = 0; i < addr1RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR)
+ .build());
+ }
+
+ for (int i = addr1RuleCount; i < addr1RuleCount + addr2RuleCount; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder()
+ .setMsgType(IPCTNL_MSG_CT_NEW)
+ .setProto(IPPROTO_TCP)
+ .setRemotePort(i)
+ .setPrivateAddress(PRIVATE_ADDR2)
+ .build());
+ }
+
+ assertConsumerCountersEquals(
+ supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0);
+
+ // Remove 1 client. Since the 1st poll will return the LastMaxCounter and
+ // update it to the current, the max counter will be kept at 1st poll, while
+ // the current counter reflect the rule decreasing.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ mConsumer.getCurrentConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ mConsumer.getLastMaxConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+ mConsumer.getLastMaxConnectionAndResetToCurrent());
+ // And all counters be updated at 2nd poll.
+ assertConsumerCountersEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0);
+
+ // Remove other client.
+ coordinator.tetherOffloadClientRemove(mIpServer, clientB);
+ assertEquals(0, mConsumer.getCurrentConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ mConsumer.getLastMaxConnectionCount());
+ assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
+ mConsumer.getLastMaxConnectionAndResetToCurrent());
+ // All counters reach zero at 2nd poll.
+ assertConsumerCountersEquals(0);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testSendActiveSessionsReported_metricsEnabled() throws Exception {
+ doTestSendActiveSessionsReported(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testSendActiveSessionsReported_metricsDisabled() throws Exception {
+ doTestSendActiveSessionsReported(false);
+ }
+
+ private void doTestSendActiveSessionsReported(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertConsumerCountersEquals(0);
+
+ // Prepare the counter value.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Then delete some 3 rules, 2 rules remaining.
+ // The max count is 5 while current rules count is 2.
+ for (int i = 0; i < 3; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Verify the method is not invoked when timer is not expired.
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+
+ // Verify metrics will be sent upon timer expiry.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(5);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify next uploaded metrics will reflect the decreased rules count.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(2);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify no metrics uploaded if polling stopped.
+ clearInvocations(mDeps);
+ coordinator.removeIpServer(mIpServer);
+ mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
private void setElapsedRealtimeNanos(long nanoSec) {
mElapsedRealtimeNanos = nanoSec;
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index fbc2893..34689bc 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -16,12 +16,19 @@
package com.android.networkstack.tethering.metrics;
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
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_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.UID_TETHERING;
import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
import static android.net.NetworkTemplate.MATCH_ETHERNET;
import static android.net.NetworkTemplate.MATCH_MOBILE;
@@ -49,29 +56,47 @@
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+import static android.stats.connectivity.UpstreamType.UT_BLUETOOTH;
+import static android.stats.connectivity.UpstreamType.UT_CELLULAR;
+import static android.stats.connectivity.UpstreamType.UT_ETHERNET;
+import static android.stats.connectivity.UpstreamType.UT_WIFI;
+
+import static com.android.networkstack.tethering.metrics.TetheringMetrics.EMPTY;
+import static com.android.testutils.NetworkStatsUtilsKt.makePublicStatsFromAndroidNetStats;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
+import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.NetworkCapabilities;
+import android.net.NetworkStats;
import android.net.NetworkTemplate;
import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.stats.connectivity.DownstreamType;
import android.stats.connectivity.ErrorCode;
import android.stats.connectivity.UpstreamType;
import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.DataUsage;
import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -90,14 +115,19 @@
private static final String GMS_PKG = "com.google.android.gms";
private static final long TEST_START_TIME = 1670395936033L;
private static final long SECOND_IN_MILLIS = 1_000L;
+ private static final long DEFAULT_TIMEOUT = 2000L;
private static final int MATCH_NONE = -1;
@Mock private Context mContext;
@Mock private Dependencies mDeps;
+ @Mock private NetworkStatsManager mNetworkStatsManager;
private TetheringMetrics mTetheringMetrics;
private final NetworkTetheringReported.Builder mStatsBuilder =
NetworkTetheringReported.newBuilder();
+ private final ArrayMap<UpstreamType, DataUsage> mMockUpstreamUsageBaseline = new ArrayMap<>();
+ private HandlerThread mThread;
+ private Handler mHandler;
private long mElapsedRealtime;
@@ -124,10 +154,35 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
doReturn(TEST_START_TIME).when(mDeps).timeNow();
+ doReturn(mNetworkStatsManager).when(mContext).getSystemService(NetworkStatsManager.class);
+ mThread = new HandlerThread("TetheringMetricsTest");
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
+ doReturn(mHandler).when(mDeps).createHandler(any());
+ // Set up the usage for upstream types.
+ mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
+ mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
+ mMockUpstreamUsageBaseline.put(UT_BLUETOOTH, new DataUsage(50L, 80L));
+ mMockUpstreamUsageBaseline.put(UT_ETHERNET, new DataUsage(0L, 0L));
+ doAnswer(inv -> {
+ final NetworkTemplate template = (NetworkTemplate) inv.getArguments()[0];
+ final DataUsage dataUsage = mMockUpstreamUsageBaseline.getOrDefault(
+ matchRuleToUpstreamType(template.getMatchRule()), new DataUsage(0L, 0L));
+ return makeNetworkStatsWithTxRxBytes(dataUsage);
+ }).when(mNetworkStatsManager).queryDetailsForUidTagState(any(), eq(Long.MIN_VALUE),
+ eq(Long.MAX_VALUE), eq(UID_TETHERING), eq(TAG_NONE), eq(STATE_ALL));
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
mElapsedRealtime = 0L;
}
+ @After
+ public void tearDown() throws Exception {
+ if (mThread != null) {
+ mThread.quitSafely();
+ mThread.join();
+ }
+ }
+
private void verifyReport(final DownstreamType downstream, final ErrorCode error,
final UserType user, final UpstreamEvents.Builder upstreamEvents, final long duration)
throws Exception {
@@ -142,9 +197,15 @@
verify(mDeps).write(expectedReport);
}
+ private void runAndWaitForIdle(Runnable r) {
+ r.run();
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
private void updateErrorAndSendReport(final int downstream, final int error) {
mTetheringMetrics.updateErrorCode(downstream, error);
mTetheringMetrics.sendReport(downstream);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
}
private static NetworkCapabilities buildUpstreamCapabilities(final int[] transports) {
@@ -176,7 +237,7 @@
private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
@@ -202,14 +263,15 @@
private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, errorCode);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, duration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
upstreamEvents, getElapsedRealtime());
clearElapsedRealtime();
@@ -243,7 +305,7 @@
private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg));
final long duration = 1 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -267,8 +329,8 @@
private void runUpstreamTypesTest(final UpstreamNetworkState ns,
final UpstreamType expectedResult) throws Exception {
mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
- mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.maybeUpdateUpstreamType(ns);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+ runAndWaitForIdle(() -> mTetheringMetrics.maybeUpdateUpstreamType(ns));
final long duration = 2 * SECOND_IN_MILLIS;
incrementCurrentTime(duration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -283,10 +345,10 @@
@Test
public void testUpstreamTypes() throws Exception {
runUpstreamTypesTest(null , UpstreamType.UT_NO_NETWORK);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UpstreamType.UT_CELLULAR);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UpstreamType.UT_WIFI);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UpstreamType.UT_BLUETOOTH);
- runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UpstreamType.UT_ETHERNET);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UT_CELLULAR);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UT_WIFI);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UT_BLUETOOTH);
+ runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UT_ETHERNET);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI_AWARE), UpstreamType.UT_WIFI_AWARE);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_LOWPAN), UpstreamType.UT_LOWPAN);
runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI,
@@ -295,13 +357,13 @@
@Test
public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(2 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG));
final long bluetoothTetheringStartTime = currentTimeMillis();
incrementCurrentTime(3 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
@@ -335,19 +397,20 @@
@Test
public void testUpstreamsWithMultipleDownstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiUpstreamStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
- mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
final long usbTetheringStartTime = currentTimeMillis();
incrementCurrentTime(5 * SECOND_IN_MILLIS);
updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder usbTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(usbTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(usbTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - usbTetheringStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_NO_ERROR,
UserType.USER_SYSTEMUI, usbTetheringUpstreamEvents,
@@ -356,7 +419,7 @@
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder wifiTetheringUpstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(wifiTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+ addUpstreamEvent(wifiTetheringUpstreamEvents, UT_WIFI,
currentTimeMillis() - wifiUpstreamStartTime, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, wifiTetheringUpstreamEvents,
@@ -365,24 +428,27 @@
@Test
public void testSwitchingMultiUpstreams() throws Exception {
- mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
final long wifiTetheringStartTime = currentTimeMillis();
incrementCurrentTime(1 * SECOND_IN_MILLIS);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
final long wifiDuration = 5 * SECOND_IN_MILLIS;
incrementCurrentTime(wifiDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
incrementCurrentTime(bluetoothDuration);
- mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR));
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
final long celltoothDuration = 20 * SECOND_IN_MILLIS;
incrementCurrentTime(celltoothDuration);
updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, wifiDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
- addUpstreamEvent(upstreamEvents, UpstreamType.UT_CELLULAR, celltoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, celltoothDuration, 0L, 0L);
verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
UserType.USER_SETTINGS, upstreamEvents,
@@ -397,10 +463,10 @@
@Test
public void testUsageSupportedForUpstreamTypeTest() {
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
- runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_CELLULAR, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_WIFI, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_BLUETOOTH, true /* isSupported */);
+ runUsageSupportedForUpstreamTypeTest(UT_ETHERNET, true /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
@@ -420,12 +486,138 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testBuildNetworkTemplateForUpstreamType() {
- runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_CELLULAR, MATCH_MOBILE);
- runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI, MATCH_WIFI);
- runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_BLUETOOTH, MATCH_BLUETOOTH);
- runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_ETHERNET, MATCH_ETHERNET);
+ runBuildNetworkTemplateForUpstreamType(UT_CELLULAR, MATCH_MOBILE);
+ runBuildNetworkTemplateForUpstreamType(UT_WIFI, MATCH_WIFI);
+ runBuildNetworkTemplateForUpstreamType(UT_BLUETOOTH, MATCH_BLUETOOTH);
+ runBuildNetworkTemplateForUpstreamType(UT_ETHERNET, MATCH_ETHERNET);
runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
}
+
+ private void verifyEmptyUsageForAllUpstreamTypes() {
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ assertEquals(EMPTY, mTetheringMetrics.getDataUsageFromUpstreamType(type));
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
+ @Test
+ public void testInitializeUpstreamDataUsageBeforeT() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage is still empty after initialization if sdk is lower than T.
+ doReturn(false).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private android.app.usage.NetworkStats makeNetworkStatsWithTxRxBytes(DataUsage dataUsage) {
+ final NetworkStats testAndroidNetStats =
+ new NetworkStats(0L /* elapsedRealtime */, 1 /* initialSize */).addEntry(
+ new NetworkStats.Entry("test", 10001, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, dataUsage.rxBytes,
+ 10, dataUsage.txBytes, 10, 10));
+ return makePublicStatsFromAndroidNetStats(testAndroidNetStats);
+ }
+
+ private static UpstreamType matchRuleToUpstreamType(int matchRule) {
+ switch (matchRule) {
+ case MATCH_MOBILE:
+ return UT_CELLULAR;
+ case MATCH_WIFI:
+ return UT_WIFI;
+ case MATCH_BLUETOOTH:
+ return UT_BLUETOOTH;
+ case MATCH_ETHERNET:
+ return UT_ETHERNET;
+ default:
+ return UpstreamType.UT_UNKNOWN;
+ }
+ }
+
+ private void initializeUpstreamUsageBaseline() {
+ doReturn(true).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+ runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testInitUpstreamUsageBaselineAndCleanup() {
+ // Verify the usage is empty for all upstream types before initialization.
+ verifyEmptyUsageForAllUpstreamTypes();
+
+ // Verify the usage has been initialized
+ initializeUpstreamUsageBaseline();
+
+ mHandler.post(() -> {
+ for (UpstreamType type : UpstreamType.values()) {
+ final DataUsage dataUsage = mTetheringMetrics.getDataUsageFromUpstreamType(type);
+ if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
+ assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
+ } else {
+ assertEquals(EMPTY, dataUsage);
+ }
+ }
+ });
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+ // Verify the usage is empty after clean up
+ runAndWaitForIdle(() -> mTetheringMetrics.cleanup());
+ verifyEmptyUsageForAllUpstreamTypes();
+ }
+
+ private void updateUpstreamDataUsage(UpstreamType type, long usageDiff) {
+ final DataUsage oldWifiUsage = mMockUpstreamUsageBaseline.get(type);
+ final DataUsage newWifiUsage = new DataUsage(
+ oldWifiUsage.txBytes + usageDiff,
+ oldWifiUsage.rxBytes + usageDiff);
+ mMockUpstreamUsageBaseline.put(type, newWifiUsage);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDataUsageCalculation() throws Exception {
+ initializeUpstreamUsageBaseline();
+ runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
+ final long wifiTetheringStartTime = currentTimeMillis();
+ incrementCurrentTime(1 * SECOND_IN_MILLIS);
+
+ // Change the upstream to Wi-Fi and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+ final long wifiDuration = 5 * SECOND_IN_MILLIS;
+ final long wifiUsageDiff = 100L;
+ incrementCurrentTime(wifiDuration);
+ updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff);
+
+ // Change the upstream to bluetooth and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
+ final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
+ final long btUsageDiff = 50L;
+ incrementCurrentTime(bluetoothDuration);
+ updateUpstreamDataUsage(UT_BLUETOOTH, btUsageDiff);
+
+ // Change the upstream to cellular and update the data usage
+ runAndWaitForIdle(() ->
+ mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
+ final long cellDuration = 20 * SECOND_IN_MILLIS;
+ final long cellUsageDiff = 500L;
+ incrementCurrentTime(cellDuration);
+ updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
+
+ // Stop tethering and verify that the data usage is uploaded.
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
+ addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, wifiUsageDiff, wifiUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, btUsageDiff, btUsageDiff);
+ addUpstreamEvent(upstreamEvents, UT_CELLULAR, cellDuration, cellUsageDiff, cellUsageDiff);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
+ UserType.USER_SETTINGS, upstreamEvents,
+ currentTimeMillis() - wifiTetheringStartTime);
+ }
}
diff --git a/bpf/headers/Android.bp b/bpf/headers/Android.bp
index d55584a..aaf8d8d 100644
--- a/bpf/headers/Android.bp
+++ b/bpf/headers/Android.bp
@@ -48,11 +48,10 @@
"BpfMapTest.cpp",
"BpfRingbufTest.cpp",
],
- defaults: ["bpf_defaults"],
+ defaults: ["bpf_cc_defaults"],
cflags: [
- "-Wall",
- "-Werror",
- "-Wno-error=unused-variable",
+ "-Wno-unused-variable",
+ "-Wno-sign-compare",
],
header_libs: ["bpf_headers"],
static_libs: ["libgmock"],
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index ca0ca76..ac5ffda 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -50,18 +50,21 @@
// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
#define BPFLOADER_MAINLINE_VERSION 42u
-// Android Mainline BpfLoader when running on Android T
+// Android Mainline BpfLoader when running on Android T (sdk=33)
#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android U
+// Android Mainline BpfLoader when running on Android U (sdk=34)
#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
// Android Mainline BpfLoader when running on Android U QPR3
#define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android V
+// Android Mainline BpfLoader when running on Android V (sdk=35)
#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android W (sdk=36)
+#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
* process the resulting .o file.
@@ -288,6 +291,12 @@
bpf_ringbuf_submit_unsafe(v, 0); \
}
+#define DEFINE_BPF_RINGBUF(the_map, ValueType, size_bytes, usr, grp, md) \
+ DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
/* There exist buggy kernels with pre-T OS, that due to
* kernel patch "[ALPS05162612] bpf: fix ubsan error"
* do not support userspace writes into non-zero index of bpf map arrays.
@@ -346,11 +355,17 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// for maps not meant to be accessed from userspace
+#define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT, \
+ 0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -389,6 +404,7 @@
static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
+// GPL only:
static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
#define bpf_printf(s, n...) bpf_trace_printk(s, sizeof(s), ## n)
// Note: bpf only supports up to 3 arguments, log via: bpf_printf("msg %d %d %d", 1, 2, 3);
diff --git a/bpf/loader/Android.bp b/bpf/loader/Android.bp
index b8c0ce7..b08913a 100644
--- a/bpf/loader/Android.bp
+++ b/bpf/loader/Android.bp
@@ -33,12 +33,7 @@
cc_binary {
name: "netbpfload",
- defaults: ["bpf_defaults"],
- cflags: [
- "-Wall",
- "-Werror",
- "-Wthread-safety",
- ],
+ defaults: ["bpf_cc_defaults"],
sanitize: {
integer_overflow: true,
},
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c058433..f369458 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -60,7 +60,14 @@
#include "bpf_map_def.h"
using android::base::EndsWith;
+using android::base::GetIntProperty;
+using android::base::GetProperty;
+using android::base::InitLogging;
+using android::base::KernelLogger;
+using android::base::SetProperty;
+using android::base::Split;
using android::base::StartsWith;
+using android::base::Tokenize;
using android::base::unique_fd;
using std::ifstream;
using std::ios;
@@ -90,6 +97,8 @@
net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
+ loader, // (U+) fs_bpf_loader /sys/fs/bpf/loader
+ // on T due to lack of sepolicy/genfscon rules it behaves simply as 'fs_bpf'
};
static constexpr domain AllDomains[] = {
@@ -99,6 +108,7 @@
domain::net_shared,
domain::netd_readonly,
domain::netd_shared,
+ domain::loader,
};
static constexpr bool specified(domain d) {
@@ -112,7 +122,7 @@
// Returns the build type string (from ro.build.type).
const std::string& getBuildType() {
- static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+ static std::string t = GetProperty("ro.build.type", "unknown");
return t;
}
@@ -131,9 +141,6 @@
#define BPF_FS_PATH "/sys/fs/bpf/"
-// Size of the BPF log buffer for verifier logging
-#define BPF_LOAD_LOG_SZ 0xfffff
-
static unsigned int page_size = static_cast<unsigned int>(getpagesize());
constexpr const char* lookupSelinuxContext(const domain d) {
@@ -144,6 +151,7 @@
case domain::net_shared: return "fs_bpf_net_shared";
case domain::netd_readonly: return "fs_bpf_netd_readonly";
case domain::netd_shared: return "fs_bpf_netd_shared";
+ case domain::loader: return "fs_bpf_loader";
}
}
@@ -167,6 +175,7 @@
case domain::net_shared: return "net_shared/";
case domain::netd_readonly: return "netd_readonly/";
case domain::netd_shared: return "netd_shared/";
+ case domain::loader: return "loader/";
}
};
@@ -184,7 +193,7 @@
static string pathToObjName(const string& path) {
// extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
- string filename = android::base::Split(path, "/").back();
+ string filename = Split(path, "/").back();
// strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
string name = filename.substr(0, filename.find_last_of('.'));
// strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
@@ -206,7 +215,7 @@
* is the name of the program, and tracepoint is the type.
*
* However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE macros.
*
* Programs shipped inside the tethering apex should be limited to networking stuff,
* as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
@@ -994,7 +1003,7 @@
(!fd.ok() ? std::strerror(errno) : "no error"));
reuse = true;
} else {
- vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
+ static char log_buf[1 << 20]; // 1 MiB logging buffer
union bpf_attr req = {
.prog_type = cs[i].type,
@@ -1002,8 +1011,8 @@
.insns = ptr_to_u64(cs[i].data.data()),
.license = ptr_to_u64(license.c_str()),
.log_level = 1,
- .log_size = static_cast<__u32>(log_buf.size()),
- .log_buf = ptr_to_u64(log_buf.data()),
+ .log_size = sizeof(log_buf),
+ .log_buf = ptr_to_u64(log_buf),
.kern_version = kvers,
.expected_attach_type = cs[i].attach_type,
};
@@ -1011,12 +1020,23 @@
strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
fd.reset(bpf(BPF_PROG_LOAD, req));
- ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
- cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
+ // Kernel should have NULL terminated the log buffer, but force it anyway for safety
+ log_buf[sizeof(log_buf) - 1] = 0;
+
+ // Strip out final newline if present
+ int log_chars = strlen(log_buf);
+ if (log_chars && log_buf[log_chars - 1] == '\n') log_buf[--log_chars] = 0;
+
+ bool log_oneline = !strchr(log_buf, '\n');
+
+ ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
+ cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
+ fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
if (!fd.ok()) {
- if (log_buf.size()) {
- vector<string> lines = android::base::Split(log_buf.data(), "\n");
+ // kernel NULL terminates log_buf, so this checks for non-empty string
+ if (log_buf[0]) {
+ vector<string> lines = Split(log_buf, "\n");
ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
for (const auto& line : lines) ALOGW("%s", line.c_str());
@@ -1085,30 +1105,22 @@
return 0;
}
-int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
+int loadProg(const char* const elfPath, const unsigned int bpfloader_ver,
const char* const prefix) {
vector<char> license;
- vector<char> critical;
vector<codeSection> cs;
vector<unique_fd> mapFds;
int ret;
- if (!isCritical) return -1;
- *isCritical = false;
-
ifstream elfFile(elfPath, ios::in | ios::binary);
if (!elfFile.is_open()) return -1;
- ret = readSectionByName("critical", elfFile, critical);
- *isCritical = !ret;
-
ret = readSectionByName("license", elfFile, license);
if (ret) {
ALOGE("Couldn't find license in %s", elfPath);
return ret;
} else {
- ALOGD("Loading %s%s ELF object %s with license %s",
- *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+ ALOGD("Loading ELF object %s with license %s",
elfPath, (char*)license.data());
}
@@ -1132,12 +1144,6 @@
ALOGD("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
- ret = readCodeSections(elfFile, cs);
- if (ret) {
- ALOGE("Couldn't read all code sections in %s", elfPath);
- return ret;
- }
-
ret = createMaps(elfPath, elfFile, mapFds, prefix, bpfloader_ver);
if (ret) {
ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
@@ -1147,6 +1153,13 @@
for (int i = 0; i < (int)mapFds.size(); i++)
ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+ ret = readCodeSections(elfFile, cs);
+ if (ret == -ENOENT) return 0; // no programs defined in this .o
+ if (ret) {
+ ALOGE("Couldn't read all code sections in %s", elfPath);
+ return ret;
+ }
+
applyMapRelo(elfFile, mapFds, cs);
ret = loadCodeSections(elfPath, cs, string(license.data()), prefix, bpfloader_ver);
@@ -1209,10 +1222,9 @@
string progPath(location.dir);
progPath += s;
- bool critical;
- int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location.prefix);
+ int ret = loadProg(progPath.c_str(), bpfloader_ver, location.prefix);
if (ret) {
- if (critical) retVal = ret;
+ retVal = ret;
ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
} else {
ALOGD("Loaded object: %s", progPath.c_str());
@@ -1247,7 +1259,7 @@
// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
// which is usually how kernel devs test the actual sysctl interfaces.
static int writeProcSysFile(const char *filename, const char *value) {
- base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+ unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
if (fd < 0) {
const int err = errno;
ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
@@ -1324,7 +1336,7 @@
}
static bool hasGSM() {
- static string ph = base::GetProperty("gsm.current.phone-type", "");
+ static string ph = GetProperty("gsm.current.phone-type", "");
static bool gsm = (ph != "");
static bool logged = false;
if (!logged) {
@@ -1337,7 +1349,7 @@
static bool isTV() {
if (hasGSM()) return false; // TVs don't do GSM
- static string key = base::GetProperty("ro.oem.key1", "");
+ static string key = GetProperty("ro.oem.key1", "");
static bool tv = StartsWith(key, "ATV00");
static bool logged = false;
if (!logged) {
@@ -1348,10 +1360,10 @@
}
static bool isWear() {
- static string wearSdkStr = base::GetProperty("ro.cw_build.wear_sdk.version", "");
- static int wearSdkInt = base::GetIntProperty("ro.cw_build.wear_sdk.version", 0);
- static string buildChars = base::GetProperty("ro.build.characteristics", "");
- static vector<string> v = base::Tokenize(buildChars, ",");
+ static string wearSdkStr = GetProperty("ro.cw_build.wear_sdk.version", "");
+ static int wearSdkInt = GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+ static string buildChars = GetProperty("ro.build.characteristics", "");
+ static vector<string> v = Tokenize(buildChars, ",");
static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
static bool wear = (wearSdkInt > 0) || watch;
static bool logged = false;
@@ -1368,7 +1380,7 @@
// Any released device will have codename REL instead of a 'real' codename.
// For safety: default to 'REL' so we default to unreleased=false on failure.
- const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+ const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
// goog/main device_api_level is bumped *way* before aosp/main api level
// (the latter only gets bumped during the push of goog/main to aosp/main)
@@ -1397,6 +1409,9 @@
const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
+ const bool isAtLeastW = (effective_api_level > __ANDROID_API_V__); // TODO: switch to W
+
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
// last in U QPR2 beta1
const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1409,6 +1424,7 @@
if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
+ if (isAtLeastW) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_W_VERSION
ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
@@ -1458,6 +1474,12 @@
if (!isTV()) return 1;
}
+ // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
+ if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
+ return 1;
+ }
+
// Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
if (isAtLeastV && isKernel32Bit() && isX86()) {
ALOGE("Android V requires X86 kernel to be 64-bit.");
@@ -1492,33 +1514,54 @@
}
}
+ /* Android 14/U should only launch on 64-bit kernels
+ * T launches on 5.10/5.15
+ * U launches on 5.15/6.1
+ * So >=5.16 implies isKernel64Bit()
+ *
+ * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+ *
+ * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+ * we also require 64-bit userspace.
+ *
+ * There are various known issues with 32-bit userspace talking to various
+ * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+ * Some of these have userspace or kernel workarounds/hacks.
+ * Some of them don't...
+ * We're going to be removing the hacks.
+ * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+ * Note: this check/enforcement only applies to *system* userspace code,
+ * it does not affect unprivileged apps, the 32-on-64 compatibility
+ * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
+ *
+ * Additionally the 32-bit kernel jit support is poor,
+ * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ */
if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
- /* Android 14/U should only launch on 64-bit kernels
- * T launches on 5.10/5.15
- * U launches on 5.15/6.1
- * So >=5.16 implies isKernel64Bit()
- *
- * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
- *
- * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
- * we also require 64-bit userspace.
- *
- * There are various known issues with 32-bit userspace talking to various
- * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
- * Some of these have userspace or kernel workarounds/hacks.
- * Some of them don't...
- * We're going to be removing the hacks.
- * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
- * Note: this check/enforcement only applies to *system* userspace code,
- * it does not affect unprivileged apps, the 32-on-64 compatibility
- * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
- *
- * Additionally the 32-bit kernel jit support is poor,
- * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
- */
- ALOGE("64-bit userspace required on 6.2+ kernels.");
- // Stuff won't work reliably, but exempt TVs & Arm Wear devices
- if (!isTV() && !(isWear() && isArm())) return 1;
+ // Stuff won't work reliably, but...
+ if (isTV()) {
+ // exempt TVs... they don't really need functional advanced networking
+ ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isWear() && isArm()) {
+ // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
+ ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+ // also exempt Arm devices upgrading with major kernel rev from T-
+ // might possibly be better for them to run with a newer kernel...
+ ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
+ } else if (isArm()) {
+ ALOGE("[Arm] 64-bit userspace required on 6.2+ kernels (%d).", first_api_level);
+ return 1;
+ } else { // x86 since RiscV cannot be 32-bit
+ ALOGE("[x86] 64-bit userspace required on 6.2+ kernels.");
+ return 1;
+ }
+ }
+
+ // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+ if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ ALOGE("64-bit userspace required on 6.7+ kernels.");
+ return 1;
}
// Ensure we can determine the Android build type.
@@ -1589,7 +1632,7 @@
int key = 1;
int value = 123;
- base::unique_fd map(
+ unique_fd map(
createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
@@ -1621,11 +1664,11 @@
} // namespace android
int main(int argc, char** argv, char * const envp[]) {
- android::base::InitLogging(argv, &android::base::KernelLogger);
+ InitLogging(argv, &KernelLogger);
if (argc == 2 && !strcmp(argv[1], "done")) {
// we're being re-exec'ed from platform bpfloader to 'finalize' things
- if (!android::base::SetProperty("bpf.progs_loaded", "1")) {
+ if (!SetProperty("bpf.progs_loaded", "1")) {
ALOGE("Failed to set bpf.progs_loaded property to 1.");
return 125;
}
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 9682545..15ab19c 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -142,11 +142,9 @@
}
if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
cg_fd, BPF_CGROUP_INET4_BIND));
- RETURN_IF_NOT_OK(attachProgramToCgroup(
- "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
cg_fd, BPF_CGROUP_INET6_BIND));
// This should trivially pass, since we just attached up above,
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index dc1f56d..52eb1b3 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -64,12 +64,6 @@
// bpf kernel programs
//
bpf {
- name: "block.o",
- srcs: ["block.c"],
- sub_dir: "net_shared",
-}
-
-bpf {
name: "dscpPolicy.o",
srcs: ["dscpPolicy.c"],
sub_dir: "net_shared",
diff --git a/bpf/progs/block.c b/bpf/progs/block.c
deleted file mode 100644
index 0e2dba9..0000000
--- a/bpf/progs/block.c
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-
-#include "bpf_net_helpers.h"
-
-DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
- 1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
-
-static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
- if (!ctx->user_port) return BPF_ALLOW;
-
- switch (ctx->protocol) {
- case IPPROTO_TCP:
- case IPPROTO_MPTCP:
- case IPPROTO_UDP:
- case IPPROTO_UDPLITE:
- case IPPROTO_DCCP:
- case IPPROTO_SCTP:
- break;
- default:
- return BPF_ALLOW; // unknown protocols are allowed
- }
-
- int key = ctx->user_port >> 6;
- int shift = ctx->user_port & 63;
-
- uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
- // Lookup should never fail in reality, but if it does return here to keep the
- // BPF verifier happy.
- if (!val) return BPF_ALLOW;
-
- if ((*val >> shift) & 1) return BPF_DISALLOW;
- return BPF_ALLOW;
-}
-
-// the program need to be accessible/loadable by netd (from netd updatable plugin)
-#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
- DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
- "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-
-DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
- return block_port(ctx);
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("ConnectivityNative");
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a86c3e6..a5664ba 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -139,6 +139,24 @@
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
+// anti-compiler-optimizer no-op: explicitly force full calculation of 'v'
+//
+// The use for this is to force full calculation of a complex arithmetic (likely binary
+// bitops) value, and then check the result only once (thus likely reducing the number
+// of required conditional jump instructions that badly affect bpf verifier runtime)
+//
+// The compiler cannot look into the assembly statement, so it doesn't know it does nothing.
+// Since the statement takes 'v' as both input and output in a register (+r),
+// the compiler must fully calculate the precise value of 'v' before this,
+// and must use the (possibly modified) value of 'v' afterwards (thus cannot
+// do funky optimizations to use partial results from before the asm).
+//
+// As this is not flagged 'volatile' this may still be moved out of a loop,
+// or even entirely optimized out if 'v' is never used afterwards.
+//
+// See: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
+#define COMPILER_FORCE_CALCULATION(v) asm ("" : "+r" (v))
+
struct egress_bool { bool egress; };
#define INGRESS ((struct egress_bool){ .egress = false })
#define EGRESS ((struct egress_bool){ .egress = true })
diff --git a/bpf/progs/dscpPolicy.c b/bpf/progs/dscpPolicy.c
index 39f2961..94d717b 100644
--- a/bpf/progs/dscpPolicy.c
+++ b/bpf/progs/dscpPolicy.c
@@ -25,12 +25,17 @@
// The cache is never read nor written by userspace and is indexed by socket cookie % CACHE_MAP_SIZE
#define CACHE_MAP_SIZE 32 // should be a power of two so we can % cheaply
-DEFINE_BPF_MAP_GRO(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry, CACHE_MAP_SIZE,
- AID_SYSTEM)
+DEFINE_BPF_MAP_KERNEL_INTERNAL(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry,
+ CACHE_MAP_SIZE)
DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+static inline __always_inline uint64_t calculate_u64(uint64_t v) {
+ COMPILER_FORCE_CALCULATION(v);
+ return v;
+}
+
static inline __always_inline void match_policy(struct __sk_buff* skb, const bool ipv4) {
void* data = (void*)(long)skb->data;
const void* data_end = (void*)(long)skb->data_end;
@@ -113,14 +118,30 @@
// this array lookup cannot actually fail
RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cacheid);
- if (existing_rule &&
- v6_equal(src_ip, existing_rule->src_ip) &&
- v6_equal(dst_ip, existing_rule->dst_ip) &&
- skb->ifindex == existing_rule->ifindex &&
- sport == existing_rule->src_port &&
- dport == existing_rule->dst_port &&
- protocol == existing_rule->proto) {
- if (existing_rule->dscp_val < 0) return;
+ if (!existing_rule) return; // impossible
+
+ uint64_t nomatch = 0;
+ nomatch |= v6_not_equal(src_ip, existing_rule->src_ip);
+ nomatch |= v6_not_equal(dst_ip, existing_rule->dst_ip);
+ nomatch |= (skb->ifindex ^ existing_rule->ifindex);
+ nomatch |= (sport ^ existing_rule->src_port);
+ nomatch |= (dport ^ existing_rule->dst_port);
+ nomatch |= (protocol ^ existing_rule->proto);
+ COMPILER_FORCE_CALCULATION(nomatch);
+
+ /*
+ * After the above funky bitwise arithmetic we have 'nomatch == 0' iff
+ * src_ip == existing_rule->src_ip &&
+ * dst_ip == existing_rule->dst_ip &&
+ * skb->ifindex == existing_rule->ifindex &&
+ * sport == existing_rule->src_port &&
+ * dport == existing_rule->dst_port &&
+ * protocol == existing_rule->proto
+ */
+
+ if (!nomatch) {
+ if (existing_rule->dscp_val < 0) return; // cached no-op
+
if (ipv4) {
uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
@@ -132,12 +153,12 @@
bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
BPF_F_RECOMPUTE_CSUM);
}
- return;
+ return; // cached DSCP mutation
}
- // Linear scan ipv4_dscp_policies_map since no stored params match skb.
- int best_score = 0;
- int8_t new_dscp = -1;
+ // Linear scan ipv?_dscp_policies_map since stored params didn't match skb.
+ uint64_t best_score = 0;
+ int8_t new_dscp = -1; // meaning no mutation
for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
// Using a uint64 in for loop prevents infinite loop during BPF load,
@@ -156,38 +177,67 @@
// easier for the verifier to analyze.
if (!policy) return;
+ // Think of 'nomatch' as a 64-bit boolean: false iff zero, true iff non-zero.
+ // Start off with nomatch being false, ie. we assume things *are* matching.
+ uint64_t nomatch = 0;
+
+ // Due to 'a ^ b' being 0 iff a == b:
+ // nomatch |= a ^ b
+ // should/can be read as:
+ // nomatch ||= (a != b)
+ // which you can also think of as:
+ // match &&= (a == b)
+
// If policy iface index does not match skb, then skip to next policy.
- if (policy->ifindex != skb->ifindex) continue;
+ nomatch |= (policy->ifindex ^ skb->ifindex);
- int score = 0;
+ // policy->match_* are normal booleans, and should thus always be 0 or 1,
+ // thus you can think of these as:
+ // if (policy->match_foo) match &&= (foo == policy->foo);
+ nomatch |= policy->match_proto * (protocol ^ policy->proto);
+ nomatch |= policy->match_src_ip * v6_not_equal(src_ip, policy->src_ip);
+ nomatch |= policy->match_dst_ip * v6_not_equal(dst_ip, policy->dst_ip);
+ nomatch |= policy->match_src_port * (sport ^ policy->src_port);
- if (policy->match_proto) {
- if (protocol != policy->proto) continue;
- score += 0xFFFF;
- }
- if (policy->match_src_ip) {
- if (v6_not_equal(src_ip, policy->src_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->match_dst_ip) {
- if (v6_not_equal(dst_ip, policy->dst_ip)) continue;
- score += 0xFFFF;
- }
- if (policy->match_src_port) {
- if (sport != policy->src_port) continue;
- score += 0xFFFF;
- }
- if (dport < policy->dst_port_start) continue;
- if (dport > policy->dst_port_end) continue;
- score += 0xFFFF + policy->dst_port_start - policy->dst_port_end;
+ // Since these values are u16s (<=63 bits), we can rely on u64 subtraction
+ // underflow setting the topmost bit. Basically, you can think of:
+ // nomatch |= (a - b) >> 63
+ // as:
+ // match &&= (a >= b)
+ uint64_t dport64 = dport; // Note: dst_port_{start_end} range is inclusive of both ends.
+ nomatch |= calculate_u64(dport64 - policy->dst_port_start) >> 63;
+ nomatch |= calculate_u64(policy->dst_port_end - dport64) >> 63;
- if (score > best_score) {
- best_score = score;
- new_dscp = policy->dscp_val;
- }
+ // score is 0x10000 for each matched field (proto, src_ip, dst_ip, src_port)
+ // plus 1..0x10000 for the dst_port range match (smaller for bigger ranges)
+ uint64_t score = 0;
+ score += policy->match_proto; // reminder: match_* are boolean, thus 0 or 1
+ score += policy->match_src_ip;
+ score += policy->match_dst_ip;
+ score += policy->match_src_port;
+ score += 1; // for a 1 element dst_port_{start,end} range
+ score <<= 16; // scale up: ie. *= 0x10000
+ // now reduce score if the dst_port range is more than a single element
+ // we want to prioritize (ie. better score) matches of smaller ranges
+ score -= (policy->dst_port_end - policy->dst_port_start); // -= 0..0xFFFF
+
+ // Here we need:
+ // match &&= (score > best_score)
+ // which is the same as
+ // match &&= (score >= best_score + 1)
+ // > not >= because we want equal score matches to prefer choosing earlier policies
+ nomatch |= calculate_u64(score - best_score - 1) >> 63;
+
+ COMPILER_FORCE_CALCULATION(nomatch);
+ if (nomatch) continue;
+
+ // only reachable if we matched the policy and (score > best_score)
+ best_score = score;
+ new_dscp = policy->dscp_val;
}
- RuleEntry value = {
+ // Update cache with found policy.
+ *existing_rule = (RuleEntry){
.src_ip = src_ip,
.dst_ip = dst_ip,
.ifindex = skb->ifindex,
@@ -197,9 +247,6 @@
.dscp_val = new_dscp,
};
- // Update cache with found policy.
- bpf_socket_policy_cache_map_update_elem(&cacheid, &value, BPF_ANY);
-
if (new_dscp < 0) return;
// Need to store bytes after updating map or program will not load.
diff --git a/bpf/progs/dscpPolicy.h b/bpf/progs/dscpPolicy.h
index 6a6b711..413fb0f 100644
--- a/bpf/progs/dscpPolicy.h
+++ b/bpf/progs/dscpPolicy.h
@@ -28,9 +28,6 @@
#define v6_not_equal(a, b) ((v6_hi_be64(a) ^ v6_hi_be64(b)) \
| (v6_lo_be64(a) ^ v6_lo_be64(b)))
-// Returns 'a == b' as boolean
-#define v6_equal(a, b) (!v6_not_equal((a), (b)))
-
typedef struct {
struct in6_addr src_ip;
struct in6_addr dst_ip;
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index 4248a46..8804ad5 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -69,6 +69,8 @@
// TODO: consider whether we can merge some of these maps
// for example it might be possible to merge 2 or 3 of:
// uid_counterset_map + uid_owner_map + uid_permission_map
+DEFINE_BPF_MAP_NO_NETD(blocked_ports_map, ARRAY, int, uint64_t,
+ 1024 /* 64K ports -> 1024 u64s */)
DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
@@ -670,6 +672,43 @@
return BPF_ALLOW;
}
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+ if (!ctx->user_port) return BPF_ALLOW;
+
+ switch (ctx->protocol) {
+ case IPPROTO_TCP:
+ case IPPROTO_MPTCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_DCCP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return BPF_ALLOW; // unknown protocols are allowed
+ }
+
+ int key = ctx->user_port >> 6;
+ int shift = ctx->user_port & 63;
+
+ uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+ // Lookup should never fail in reality, but if it does return here to keep the
+ // BPF verifier happy.
+ if (!val) return BPF_ALLOW;
+
+ if ((*val >> shift) & 1) return BPF_DISALLOW;
+ return BPF_ALLOW;
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index 4877a4b..be7c311 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -157,6 +157,8 @@
#define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
#define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_BIND4_PROG_PATH BPF_NETD_PATH "prog_netd_bind4_inet4_bind"
+#define CGROUP_BIND6_PROG_PATH BPF_NETD_PATH "prog_netd_bind6_inet6_bind"
#define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
#define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
#define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
diff --git a/bpf/progs/test.c b/bpf/progs/test.c
index bce402e..8585118 100644
--- a/bpf/progs/test.c
+++ b/bpf/progs/test.c
@@ -42,22 +42,13 @@
// Used only by BpfBitmapTest, not by production code.
DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, AID_NETWORK_STACK)
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
- xdp_test, KVER_5_9)
-(struct xdp_md *ctx) {
- void *data = (void *)(long)ctx->data;
- void *data_end = (void *)(long)ctx->data_end;
-
- struct ethhdr *eth = data;
- int hsize = sizeof(*eth);
-
- struct iphdr *ip = data + hsize;
- hsize += sizeof(struct iphdr);
-
- if (data + hsize > data_end) return XDP_PASS;
- if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;
- if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
- return XDP_PASS;
+// we need at least 1 bpf program in the final .o for Android S bpfloader compatibility
+// this program is trivial, and has a 'infinite' minimum kernel version number,
+// so will always be skipped
+DEFINE_BPF_PROG_KVER("skfilter/match", AID_ROOT, AID_ROOT, match, KVER_INF)
+(__unused struct __sk_buff* skb) {
+ return XTBPF_MATCH;
}
LICENSE("Apache 2.0");
+CRITICAL("Networking xTS tests");
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 29f5cd2..eaa6373 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -80,20 +80,15 @@
TETHERING "prog_offload_schedcls_tether_upstream6_rawip",
};
-// Provided by *current* mainline module for S+ devices with 5.10+ kernels
-static const set<string> MAINLINE_FOR_S_5_10_PLUS = {
- TETHERING "prog_test_xdp_drop_ipv4_udp_ether",
-};
-
// Provided by *current* mainline module for T+ devices
static const set<string> MAINLINE_FOR_T_PLUS = {
- SHARED "map_block_blocked_ports_map",
SHARED "map_clatd_clat_egress4_map",
SHARED "map_clatd_clat_ingress6_map",
SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
SHARED "map_dscpPolicy_socket_policy_cache_map",
NETD "map_netd_app_uid_stats_map",
+ NETD "map_netd_blocked_ports_map",
NETD "map_netd_configuration_map",
NETD "map_netd_cookie_tag_map",
NETD "map_netd_data_saver_enabled_map",
@@ -124,8 +119,8 @@
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
- NETD_RO "prog_block_bind4_block_port",
- NETD_RO "prog_block_bind6_block_port",
+ NETD "prog_netd_bind4_inet4_bind",
+ NETD "prog_netd_bind6_inet6_bind",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -159,7 +154,7 @@
NETD "prog_netd_setsockopt_prog",
};
-// Provided by *current* mainline module for U+ devices with 5.10+ kernels
+// Provided by *current* mainline module for V+ devices with 5.10+ kernels
static const set<string> MAINLINE_FOR_V_5_10_PLUS = {
NETD "prog_netd_cgroupsockrelease_inet_release",
};
@@ -194,7 +189,6 @@
// S requires Linux Kernel 4.9+ and thus requires eBPF support.
if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
- DO_EXPECT(IsAtLeastS() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_S_5_10_PLUS);
// Nothing added or removed in SCv2.
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index a05a529..7551b92 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -68,8 +68,8 @@
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
// Compile against the entire implementation of framework-connectivity,
// including hidden methods. This is safe because if framework-connectivity-t is
// on the bootclasspath (i.e., T), then framework-connectivity is also on the
@@ -103,8 +103,8 @@
name: "framework-connectivity-t-pre-jarjar",
defaults: ["framework-connectivity-t-defaults"],
libs: [
- "framework-bluetooth",
- "framework-wifi",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-location.stubs.module_lib",
],
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index 7f0c1fe..01ac106 100644
--- a/framework-t/src/android/net/INetworkStatsService.aidl
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -78,13 +78,16 @@
void unregisterUsageRequest(in DataUsageRequest request);
/** Get the uid stats information since boot */
- long getUidStats(int uid, int type);
+ NetworkStats getTypelessUidStats(int uid);
/** Get the iface stats information since boot */
- long getIfaceStats(String iface, int type);
+ NetworkStats getTypelessIfaceStats(String iface);
/** Get the total network stats information since boot */
- long getTotalStats(int type);
+ NetworkStats getTypelessTotalStats();
+
+ /** Get the uid stats information (with specified type) since boot */
+ long getUidStats(int uid, int type);
/** Registers a network stats provider */
INetworkStatsProviderCallback registerNetworkStatsProvider(String tag,
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 77c8001..3b6a69b 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -40,6 +40,9 @@
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
+import java.util.Iterator;
+import java.util.Objects;
+
/**
* Class that provides network traffic statistics. These statistics include
@@ -730,11 +733,7 @@
* @return The number of transmitted packets.
*/
public static long getTxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_PACKETS);
}
/**
@@ -753,11 +752,7 @@
* @return The number of received packets.
*/
public static long getRxPackets(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_PACKETS);
}
/**
@@ -776,11 +771,7 @@
* @return The number of transmitted bytes.
*/
public static long getTxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_TX_BYTES);
}
/**
@@ -799,51 +790,31 @@
* @return The number of received bytes.
*/
public static long getRxBytes(@NonNull String iface) {
- try {
- return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(iface, TYPE_RX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxPackets() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
}
/** {@hide} */
@TestApi
public static long getLoopbackTxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
}
/** {@hide} */
@TestApi
public static long getLoopbackRxBytes() {
- try {
- return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
}
/**
@@ -856,11 +827,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_PACKETS);
}
/**
@@ -873,11 +840,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxPackets() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_PACKETS);
}
/**
@@ -890,11 +853,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_TX_BYTES);
}
/**
@@ -907,11 +866,7 @@
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxBytes() {
- try {
- return getStatsService().getTotalStats(TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getTotalStats(TYPE_RX_BYTES);
}
/**
@@ -933,11 +888,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_BYTES);
}
/**
@@ -959,11 +910,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxBytes(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_RX_BYTES);
}
/**
@@ -985,11 +932,7 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidTxPackets(int uid) {
- try {
- return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return getUidStats(uid, TYPE_TX_PACKETS);
}
/**
@@ -1011,11 +954,50 @@
* @see android.content.pm.ApplicationInfo#uid
*/
public static long getUidRxPackets(int uid) {
+ return getUidStats(uid, TYPE_RX_PACKETS);
+ }
+
+ /** @hide */
+ public static long getUidStats(int uid, int type) {
+ if (!isEntryValueTypeValid(type)
+ || android.os.Process.myUid() != uid) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
try {
- return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+ stats = getStatsService().getTypelessUidStats(uid);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
+ return getValueForTypeFromFirstEntry(stats, type);
+ }
+
+ /** @hide */
+ public static long getTotalStats(int type) {
+ if (!isEntryValueTypeValid(type)) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
+ try {
+ stats = getStatsService().getTypelessTotalStats();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getValueForTypeFromFirstEntry(stats, type);
+ }
+
+ /** @hide */
+ public static long getIfaceStats(String iface, int type) {
+ if (!isEntryValueTypeValid(type)) {
+ return UNSUPPORTED;
+ }
+ final NetworkStats stats;
+ try {
+ stats = getStatsService().getTypelessIfaceStats(iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return getValueForTypeFromFirstEntry(stats, type);
}
/**
@@ -1143,4 +1125,45 @@
public static final int TYPE_TX_BYTES = 2;
/** {@hide} */
public static final int TYPE_TX_PACKETS = 3;
+
+ /** @hide */
+ private static long getEntryValueForType(@NonNull NetworkStats.Entry entry, int type) {
+ Objects.requireNonNull(entry);
+ if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+ switch (type) {
+ case TYPE_RX_BYTES:
+ return entry.getRxBytes();
+ case TYPE_RX_PACKETS:
+ return entry.getRxPackets();
+ case TYPE_TX_BYTES:
+ return entry.getTxBytes();
+ case TYPE_TX_PACKETS:
+ return entry.getTxPackets();
+ default:
+ throw new IllegalStateException("Bug: Invalid type: "
+ + type + " should not reach here.");
+ }
+ }
+
+ /** @hide */
+ private static boolean isEntryValueTypeValid(int type) {
+ switch (type) {
+ case TYPE_RX_BYTES:
+ case TYPE_RX_PACKETS:
+ case TYPE_TX_BYTES:
+ case TYPE_TX_PACKETS:
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ /** @hide */
+ public static long getValueForTypeFromFirstEntry(@NonNull NetworkStats stats, int type) {
+ Objects.requireNonNull(stats);
+ Iterator<NetworkStats.Entry> iter = stats.iterator();
+ if (!iter.hasNext()) return UNSUPPORTED;
+ return getEntryValueForType(iter.next(), type);
+ }
}
+
diff --git a/framework/Android.bp b/framework/Android.bp
index 4c4f792..0334e11 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -178,8 +178,10 @@
// In preparation for future move
"//packages/modules/Connectivity/apex",
"//packages/modules/Connectivity/framework-t",
+ "//packages/modules/Connectivity/remoteauth/service",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/staticlibs",
"//frameworks/base",
// Tests using hidden APIs
@@ -201,6 +203,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
+ "//packages/modules/NetworkStack",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 63a6cd2..1ebc4a3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -6734,4 +6734,33 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Get the specified ConnectivityService feature status. This method is for test code to check
+ * whether the feature is enabled or not.
+ * Note that tests can not just read DeviceConfig since ConnectivityService reads flag at
+ * startup. For example, it's possible that the current flag value is "disable"(-1) but the
+ * feature is enabled since the flag value was "enable"(1) when ConnectivityService started up.
+ * If the ConnectivityManager needs to check the ConnectivityService feature status for non-test
+ * purpose, define feature in {@link ConnectivityManagerFeature} and use
+ * {@link #isFeatureEnabled} instead.
+ *
+ * @param featureFlag target flag for feature
+ * @return {@code true} if the feature is enabled, {@code false} if the feature is disabled.
+ * @throws IllegalArgumentException if the flag is invalid
+ *
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public boolean isConnectivityServiceFeatureEnabledForTesting(final String featureFlag) {
+ try {
+ return mService.isConnectivityServiceFeatureEnabledForTesting(featureFlag);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 988cc92..47b3316 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -262,4 +262,6 @@
IBinder getRoutingCoordinatorService();
long getEnabledConnectivityManagerFeatures();
+
+ boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
}
diff --git a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index 6e87ed3..ba39ca0 100644
--- a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -24,8 +24,8 @@
import androidx.annotation.RequiresApi;
/**
- * Utility providing limited access to module-internal APIs which are only available on Android T+,
- * as this class is only in the bootclasspath on T+ as part of framework-connectivity.
+ * Utility providing limited access to module-internal APIs which are only available on Android S+,
+ * as this class is only in the bootclasspath on S+ as part of framework-connectivity.
*
* R+ module components like Tethering cannot depend on all hidden symbols from
* framework-connectivity. They only have access to stable API stubs where newer APIs can be
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index f84ddcf..6bfa54d 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -49,7 +49,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"framework-location.stubs.module_lib",
],
static_libs: [
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 749113d..c9c7b44 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -35,11 +35,11 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"androidx.core_core",
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 8009303..9d42dd1 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -30,9 +30,9 @@
"truth",
],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
"framework-bluetooth.stubs.module_lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-t.impl",
"framework-location.stubs.module_lib",
],
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 9e1ec70..472f4f0 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,7 +21,6 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner"/>
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 2950568..4d2d1d5 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -27,9 +27,9 @@
srcs: ["src/**/*.java"],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
],
compile_multilib: "both",
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index e33abd5..52667ae 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -27,7 +27,7 @@
],
libs: [
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"service-connectivity-pre-jarjar",
],
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index f35b163..b2ef345 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -15,39 +15,68 @@
*/
package com.android.server.net.ct;
+import android.annotation.RequiresApi;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
+import android.os.Build;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
/** Helper class to download certificate transparency log files. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CertificateTransparencyDownloader extends BroadcastReceiver {
private static final String TAG = "CertificateTransparencyDownloader";
+ // TODO: move key to a DeviceConfig flag.
+ private static final byte[] PUBLIC_KEY_BYTES =
+ Base64.getDecoder()
+ .decode(
+ "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsu0BHGnQ++W2CTdyZyxv"
+ + "HHRALOZPlnu/VMVgo2m+JZ8MNbAOH2cgXb8mvOj8flsX/qPMuKIaauO+PwROMjiq"
+ + "fUpcFm80Kl7i97ZQyBDYKm3MkEYYpGN+skAR2OebX9G2DfDqFY8+jUpOOWtBNr3L"
+ + "rmVcwx+FcFdMjGDlrZ5JRmoJ/SeGKiORkbbu9eY1Wd0uVhz/xI5bQb0OgII7hEj+"
+ + "i/IPbJqOHgB8xQ5zWAJJ0DmG+FM6o7gk403v6W3S8qRYiR84c50KppGwe4YqSMkF"
+ + "bLDleGQWLoaDSpEWtESisb4JiLaY4H+Kk0EyAhPSb+49JfUozYl+lf7iFN3qRq/S"
+ + "IXXTh6z0S7Qa8EYDhKGCrpI03/+qprwy+my6fpWHi6aUIk4holUCmWvFxZDfixox"
+ + "K0RlqbFDl2JXMBquwlQpm8u5wrsic1ksIv9z8x9zh4PJqNpCah0ciemI3YGRQqSe"
+ + "/mRRXBiSn9YQBUPcaeqCYan+snGADFwHuXCd9xIAdFBolw9R9HTedHGUfVXPJDiF"
+ + "4VusfX6BRR/qaadB+bqEArF/TzuDUr6FvOR4o8lUUxgLuZ/7HO+bHnaPFKYHHSm+"
+ + "+z1lVDhhYuSZ8ax3T0C3FZpb7HMjZtpEorSV5ElKJEJwrhrBCMOD8L01EoSPrGlS"
+ + "1w22i9uGHMn/uGQKo28u7AsCAwEAAQ==");
+
private final Context mContext;
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
private final CertificateTransparencyInstaller mInstaller;
+ private final byte[] mPublicKey;
@VisibleForTesting
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
- CertificateTransparencyInstaller installer) {
+ CertificateTransparencyInstaller installer,
+ byte[] publicKey) {
mContext = context;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
mInstaller = installer;
+ mPublicKey = publicKey;
}
CertificateTransparencyDownloader(Context context, DataStore dataStore) {
@@ -55,13 +84,14 @@
context,
dataStore,
new DownloadHelper(context),
- new CertificateTransparencyInstaller());
+ new CertificateTransparencyInstaller(),
+ PUBLIC_KEY_BYTES);
}
void registerReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter);
+ mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
@@ -139,12 +169,22 @@
return;
}
- // TODO: 1. verify file signature, 2. validate file content.
+ boolean success = false;
+ try {
+ success = verify(contentUri, metadataUri);
+ } catch (IOException | GeneralSecurityException e) {
+ Log.e(TAG, "Could not verify new log list", e);
+ }
+ if (!success) {
+ Log.w(TAG, "Log list did not pass verification");
+ return;
+ }
+
+ // TODO: validate file content.
String version = mDataStore.getProperty(Config.VERSION_PENDING);
String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
- boolean success = false;
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
success = mInstaller.install(inputStream, version);
} catch (IOException e) {
@@ -161,6 +201,19 @@
}
}
+ private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ verifier.initVerify(keyFactory.generatePublic(new X509EncodedKeySpec(mPublicKey)));
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ verifier.update(fileStream.readAllBytes());
+ return verifier.verify(signatureStream.readAllBytes());
+ }
+ }
+
private long download(String url) {
try {
return mDownloadHelper.startDownload(url);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index fdac434..a263546 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -15,19 +15,18 @@
*/
package com.android.server.net.ct;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
-
+import android.annotation.RequiresApi;
import android.content.Context;
+import android.os.Build;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.text.TextUtils;
import android.util.Log;
-import com.android.modules.utils.build.SdkLevel;
-
import java.util.concurrent.Executors;
/** Listener class for the Certificate Transparency Phenotype flags. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
private static final String TAG = "CertificateTransparencyFlagsListener";
@@ -45,7 +44,7 @@
mDataStore.load();
mCertificateTransparencyDownloader.registerReceiver();
DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+ Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
}
@@ -54,14 +53,18 @@
@Override
public void onPropertiesChanged(Properties properties) {
- if (!SdkLevel.isAtLeastV() || !NAMESPACE_TETHERING.equals(properties.getNamespace())) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
return;
}
- String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, Config.VERSION, "");
- String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, Config.CONTENT_URL, "");
+ String newVersion =
+ DeviceConfig.getString(Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_VERSION, "");
+ String newContentUrl =
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_CONTENT_URL, "");
String newMetadataUrl =
- DeviceConfig.getString(NAMESPACE_TETHERING, Config.METADATA_URL, "");
+ DeviceConfig.getString(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_METADATA_URL, "");
if (TextUtils.isEmpty(newVersion)
|| TextUtils.isEmpty(newContentUrl)
|| TextUtils.isEmpty(newMetadataUrl)) {
@@ -85,6 +88,8 @@
return;
}
+ // TODO: handle the case where there is already a pending download.
+
mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 52478c0..edf7c56 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -19,27 +19,23 @@
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
+import android.provider.DeviceConfig;
import com.android.net.ct.flags.Flags;
-import com.android.net.module.util.DeviceConfigUtils;
import com.android.server.SystemService;
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
- private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
- "certificate_transparency_service_enabled";
-
private final CertificateTransparencyFlagsListener mFlagsListener;
/**
* @return true if the CertificateTransparency service is enabled.
*/
public static boolean enabled(Context context) {
- // TODO: replace isTetheringFeatureEnabled with CT namespace flag.
- return DeviceConfigUtils.isTetheringFeatureEnabled(
- context, CERTIFICATE_TRANSPARENCY_ENABLED)
+ return DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
&& Flags.certificateTransparencyService();
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 04b7dac..2a6b8e2 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -33,7 +33,15 @@
private static final String PREFERENCES_FILE_NAME = "ct.preferences";
static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
- // flags and properties names
+ // Phenotype flags
+ static final String NAMESPACE_NETWORK_SECURITY = "network_security";
+ private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
+ static final String FLAG_SERVICE_ENABLED = FLAGS_PREFIX + "service_enabled";
+ static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
+ static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
+ static final String FLAG_VERSION = FLAGS_PREFIX + "version";
+
+ // properties
static final String VERSION_PENDING = "version_pending";
static final String VERSION = "version";
static final String CONTENT_URL_PENDING = "content_url_pending";
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
index 639f644..11263cf 100644
--- a/networksecurity/tests/unit/Android.bp
+++ b/networksecurity/tests/unit/Android.bp
@@ -27,9 +27,9 @@
srcs: ["src/**/*.java"],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
],
static_libs: [
"androidx.test.ext.junit",
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 5131a71..a056c35 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -40,7 +40,17 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
/** Tests for the {@link CertificateTransparencyDownloader}. */
@RunWith(JUnit4.class)
@@ -49,15 +59,20 @@
@Mock private DownloadHelper mDownloadHelper;
@Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+ private PrivateKey mPrivateKey;
private Context mContext;
private File mTempFile;
private DataStore mDataStore;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
@Before
- public void setUp() throws IOException {
+ public void setUp() throws IOException, NoSuchAlgorithmException {
MockitoAnnotations.initMocks(this);
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ KeyPair keyPair = instance.generateKeyPair();
+ mPrivateKey = keyPair.getPrivate();
+
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mTempFile = File.createTempFile("datastore-test", ".properties");
mDataStore = new DataStore(mTempFile);
@@ -65,7 +80,11 @@
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
- mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
+ mContext,
+ mDataStore,
+ mDownloadHelper,
+ mCertificateTransparencyInstaller,
+ keyPair.getPublic().getEncoded());
}
@After
@@ -128,23 +147,16 @@
}
@Test
- public void testDownloader_handleContentCompleteInstallSuccessful() throws IOException {
+ public void testDownloader_handleContentCompleteInstallSuccessful() throws Exception {
String version = "666";
- mDataStore.setProperty(Config.VERSION_PENDING, version);
-
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
-
long contentId = 666;
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
@@ -161,23 +173,16 @@
}
@Test
- public void testDownloader_handleContentCompleteInstallFails() throws IOException {
+ public void testDownloader_handleContentCompleteInstallFails() throws Exception {
String version = "666";
- mDataStore.setProperty(Config.VERSION_PENDING, version);
-
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
-
long contentId = 666;
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ File logListFile = File.createTempFile("log_list", "json");
+ Uri contentUri = Uri.fromFile(logListFile);
+ long metadataId = 123;
+ File metadataFile = sign(logListFile);
+ Uri metadataUri = Uri.fromFile(metadataFile);
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
mCertificateTransparencyDownloader.onReceive(
@@ -188,8 +193,56 @@
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
}
+ @Test
+ public void testDownloader_handleContentCompleteVerificationFails() throws IOException {
+ String version = "666";
+ long contentId = 666;
+ Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+ long metadataId = 123;
+ Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-wrong_metadata", "sig"));
+
+ setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ }
+
private Intent makeDownloadCompleteIntent(long downloadId) {
return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
+
+ private void setUpDownloadComplete(
+ String version, long metadataId, Uri metadataUri, long contentId, Uri contentUri)
+ throws IOException {
+ mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+ when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+ when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ }
+
+ private File sign(File file) throws IOException, GeneralSecurityException {
+ File signatureFile = File.createTempFile("log_list-metadata", "sig");
+ Signature signer = Signature.getInstance("SHA256withRSA");
+ signer.initSign(mPrivateKey);
+
+ try (InputStream fileStream = new FileInputStream(file);
+ OutputStream outputStream = new FileOutputStream(signatureFile)) {
+ signer.update(fileStream.readAllBytes());
+ outputStream.write(signer.sign());
+ }
+
+ return signatureFile;
+ }
}
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 2f1737f..33de139 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -47,7 +47,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-bluetooth",
+ "framework-bluetooth.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 32ae54f..52f301a 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -33,13 +33,13 @@
],
libs: [
"androidx.annotation_annotation",
- "framework-bluetooth",
- "framework-connectivity",
+ "framework-bluetooth.stubs.module_lib",
+ "framework-connectivity.impl",
"error_prone_annotations",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
- "framework-statsd",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"modules-utils-build",
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index fc91e0c..57e3ec9 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -13,7 +13,6 @@
rustlibs: [
"libbinder_rs",
"libjni_legacy",
- "liblazy_static",
"liblog_rust",
"liblogger",
"libnum_traits",
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 421fe7e..9add6df 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -21,12 +21,11 @@
use jni::signature::TypeSignature;
use jni::sys::{jbyteArray, jint, jlong, jvalue};
use jni::{JNIEnv, JavaVM};
-use lazy_static::lazy_static;
use log::{debug, error, info};
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicI64, Ordering},
- Arc, Mutex,
+ Arc, LazyLock, Mutex,
};
/// Macro capturing the name of the function calling this macro.
@@ -51,11 +50,9 @@
}};
}
-lazy_static! {
- static ref HANDLE_MAPPING: Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>> =
- Mutex::new(HashMap::new());
- static ref HANDLE_RN: AtomicI64 = AtomicI64::new(0);
-}
+static HANDLE_MAPPING: LazyLock<Mutex<HashMap<i64, Arc<Mutex<JavaPlatform>>>>> =
+ LazyLock::new(|| Mutex::new(HashMap::new()));
+static HANDLE_RN: AtomicI64 = AtomicI64::new(0);
fn generate_platform_handle() -> i64 {
HANDLE_RN.fetch_add(1, Ordering::SeqCst)
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 47b9e31..f784b8e 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -30,9 +30,9 @@
srcs: [],
libs: [
- "android.test.base",
- "android.test.mock",
- "android.test.runner",
+ "android.test.base.stubs.test",
+ "android.test.mock.stubs.test",
+ "android.test.runner.stubs.test",
"framework-annotations-lib",
],
compile_multilib: "both",
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 32dbcaa..787e94e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -51,12 +51,12 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
// TODO: use framework-tethering-pre-jarjar when it is separated from framework-tethering
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"service-nearby-pre-jarjar",
"service-networksecurity-pre-jarjar",
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 241d5fa..9cca078 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -41,10 +41,7 @@
// The task runner is sequential so these can't run on top of each other.
runner->PostDelayedTask([=, this]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
- if (mMutex.try_lock()) {
- ConsumeAllLocked();
- mMutex.unlock();
- }
+ ConsumeAll();
}
bool NetworkTracePoller::Start(uint32_t pollMs) {
@@ -76,7 +73,10 @@
return false;
}
- mRingBuffer = std::move(*rb);
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer = std::move(*rb);
+ }
auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
if (!res.ok()) {
@@ -114,10 +114,14 @@
// Drain remaining events from the ring buffer now that tracing is disabled.
// This prevents the next trace from seeing stale events and allows writing
// the last batch of events to Perfetto.
- ConsumeAllLocked();
+ ConsumeAll();
mTaskRunner.reset();
- mRingBuffer.reset();
+
+ {
+ std::scoped_lock<std::mutex> block(mBufferMutex);
+ mRingBuffer.reset();
+ }
return res.ok();
}
@@ -145,22 +149,20 @@
}
bool NetworkTracePoller::ConsumeAll() {
- std::scoped_lock<std::mutex> lock(mMutex);
- return ConsumeAllLocked();
-}
-
-bool NetworkTracePoller::ConsumeAllLocked() {
- if (mRingBuffer == nullptr) {
- ALOGW("Tracing is not active");
- return false;
- }
-
std::vector<PacketTrace> packets;
- base::Result<int> ret = mRingBuffer->ConsumeAll(
- [&](const PacketTrace& pkt) { packets.push_back(pkt); });
- if (!ret.ok()) {
- ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
- return false;
+ {
+ std::scoped_lock<std::mutex> lock(mBufferMutex);
+ if (mRingBuffer == nullptr) {
+ ALOGW("Tracing is not active");
+ return false;
+ }
+
+ base::Result<int> ret = mRingBuffer->ConsumeAll(
+ [&](const PacketTrace& pkt) { packets.push_back(pkt); });
+ if (!ret.ok()) {
+ ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+ return false;
+ }
}
ATRACE_INT("NetworkTracePackets", packets.size());
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index 092ab64..72fa66e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -50,7 +50,7 @@
bool Stop() EXCLUDES(mMutex);
// Consumes all available events from the ringbuffer.
- bool ConsumeAll() EXCLUDES(mMutex);
+ bool ConsumeAll() EXCLUDES(mBufferMutex);
private:
// Poll the ring buffer for new data and schedule another run of ourselves
@@ -59,15 +59,19 @@
// and thus a deadlock while resetting the TaskRunner. The runner pointer is
// always valid within tasks run by that runner.
void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
- bool ConsumeAllLocked() REQUIRES(mMutex);
// Record sparse iface stats via atrace. This queries the per-iface stats maps
// for any iface present in the vector of packets. This is inexact, but should
// have sufficient coverage given these are cumulative counters.
- void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+ static void TraceIfaces(const std::vector<PacketTrace>& packets);
std::mutex mMutex;
+ // The mBufferMutex protects the ring buffer. This allows separate protected
+ // access of mTaskRunner in Stop (to terminate) and mRingBuffer in ConsumeAll.
+ // Without this separation, Stop() can deadlock.
+ std::mutex mBufferMutex;
+
// Records the number of successfully started active sessions so that only the
// first active session attempts setup and only the last cleans up. Note that
// the session count will remain zero if Start fails. It is expected that Stop
@@ -78,10 +82,10 @@
uint32_t mPollMs GUARDED_BY(mMutex);
// The function to process PacketTrace, typically a Perfetto sink.
- EventSink mCallback GUARDED_BY(mMutex);
+ const EventSink mCallback;
// The BPF ring buffer handle.
- std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mMutex);
+ std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mBufferMutex);
// The packet tracing config map (really a 1-element array).
BpfMap<uint32_t, bool> mConfigurationMap GUARDED_BY(mMutex);
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 5f672e7..0adb290 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1938,8 +1938,25 @@
mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
.setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
- .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
- mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
+ .setIsCachedServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_CACHED_SERVICES_REMOVAL))
+ .setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
+ MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
+ MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+ .setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
+ @Override
+ public boolean isForceEnabledForTest(@NonNull String flag) {
+ return mDeps.isFeatureEnabled(
+ mContext,
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag);
+ }
+
+ @Override
+ public int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ return mDeps.getDeviceConfigPropertyInt(
+ FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag, defaultValue);
+ }
+ })
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2006,6 +2023,14 @@
}
/**
+ * @see DeviceConfigUtils#getDeviceConfigPropertyInt
+ */
+ public int getDeviceConfigPropertyInt(String feature, int defaultValue) {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ NAMESPACE_TETHERING, feature, defaultValue);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 7fa605a..b16d8bd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -134,13 +136,20 @@
this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
}
- private static class DiscoveryExecutor implements Executor {
+ /**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ static class DiscoveryExecutor implements Executor {
private final HandlerThread handlerThread;
@GuardedBy("pendingTasks")
@Nullable private Handler handler;
+ // Store pending tasks and associated delay time. Each Pair represents a pending task
+ // (first) and its delay time (second).
@GuardedBy("pendingTasks")
- @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+ @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
DiscoveryExecutor(@Nullable Looper defaultLooper) {
if (defaultLooper != null) {
@@ -154,8 +163,8 @@
protected void onLooperPrepared() {
synchronized (pendingTasks) {
handler = new Handler(getLooper());
- for (Runnable pendingTask : pendingTasks) {
- handler.post(pendingTask);
+ for (Pair<Runnable, Long> pendingTask : pendingTasks) {
+ handler.postDelayed(pendingTask.first, pendingTask.second);
}
pendingTasks.clear();
}
@@ -177,16 +186,20 @@
@Override
public void execute(Runnable function) {
+ executeDelayed(function, 0L /* delayMillis */);
+ }
+
+ public void executeDelayed(Runnable function, long delayMillis) {
final Handler handler;
synchronized (pendingTasks) {
if (this.handler == null) {
- pendingTasks.add(function);
+ pendingTasks.add(Pair.create(function, delayMillis));
return;
} else {
handler = this.handler;
}
}
- handler.post(function);
+ handler.postDelayed(function, delayMillis);
}
void shutDown() {
@@ -288,6 +301,17 @@
serviceTypeClient.notifySocketDestroyed();
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected,
+ // the service type client won't receive any updates for them. Therefore,
+ // remove these cached services after exceeding the retention time
+ // (currently 10s) if no service type client requires them.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey =
+ serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
});
}
@@ -324,6 +348,42 @@
// of the service type clients.
executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
perSocketServiceTypeClients.remove(serviceTypeClient);
+ // The cached services may not be reliable after the socket is disconnected, the
+ // service type client won't receive any updates for them. Therefore, remove these
+ // cached services after exceeding the retention time (currently 10s) if no service
+ // type client requires them.
+ // Note: This removal is only called if the requested socket is still active for
+ // other requests. If the requested socket is no longer needed after the listener
+ // is unregistered, SocketCreationCallback#onSocketDestroyed callback will remove
+ // both the service type client and cached services there.
+ //
+ // List some multiple listener cases for the cached service removal flow.
+ //
+ // Case 1 - Same service type, different network requests
+ // - Register Listener A (service type X, requesting all networks: Y and Z)
+ // - Create service type clients X-Y and X-Z
+ // - Register Listener B (service type X, requesting network Y)
+ // - Reuse service type client X-Y
+ // - Unregister Listener A
+ // - Socket destroyed on network Z; remove the X-Z client. Unregister the listener
+ // from the X-Y client and keep it, as it's still being used by Listener B.
+ // - Remove cached services associated with the X-Z client after 10 seconds.
+ //
+ // Case 2 - Different service types, same network request
+ // - Register Listener A (service type X, requesting network Y)
+ // - Create service type client X-Y
+ // - Register Listener B (service type Z, requesting network Y)
+ // - Create service type client Z-Y
+ // - Unregister Listener A
+ // - No socket is destroyed because network Y is still being used by Listener B.
+ // - Unregister the listener from the X-Y client, then remove it.
+ // - Remove cached services associated with the X-Y client after 10 seconds.
+ if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
+ final MdnsServiceCache.CacheKey cacheKey = serviceTypeClient.getCacheKey();
+ discoveryExecutor.executeDelayed(
+ () -> handleRemoveCachedServices(cacheKey),
+ mdnsFeatureFlags.getCachedServicesRetentionTime());
+ }
}
}
if (perSocketServiceTypeClients.isEmpty()) {
@@ -368,6 +428,26 @@
}
}
+ private void handleRemoveCachedServices(@NonNull MdnsServiceCache.CacheKey cacheKey) {
+ // Check if there is an active service type client that requires the cached services. If so,
+ // do not remove associated services from cache.
+ for (MdnsServiceTypeClient client : getMdnsServiceTypeClient(cacheKey.mSocketKey)) {
+ if (client.getCacheKey().equals(cacheKey)) {
+ // Found a client that has same CacheKey.
+ return;
+ }
+ }
+ sharedLog.log("Remove cached services for " + cacheKey);
+ // No client has same CacheKey. Remove associated services.
+ getServiceCache().removeServices(cacheKey);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ MdnsServiceCache getServiceCache() {
+ return serviceCache;
+ }
+
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 709dc79..4e27fef 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -73,6 +73,22 @@
public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
"nsd_avoid_advertising_empty_txt_records";
+ /**
+ * A feature flag to control whether the cached services removal should be enabled.
+ * The removal will be triggered if the retention time has elapsed after all listeners have been
+ * unregistered from the service type client or the interface has been destroyed.
+ */
+ public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
+
+ /**
+ * A feature flag to control the retention time for cached services.
+ *
+ * <p> Making the retention time configurable allows for testing and future adjustments.
+ */
+ public static final String NSD_CACHED_SERVICES_RETENTION_TIME =
+ "nsd_cached_services_retention_time";
+ public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -100,6 +116,12 @@
// Flag for avoiding advertising empty TXT records
public final boolean mAvoidAdvertisingEmptyTxtRecords;
+ // Flag for cached services removal
+ public final boolean mIsCachedServicesRemovalEnabled;
+
+ // Retention Time for cached services
+ public final long mCachedServicesRetentionTime;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -111,6 +133,12 @@
* Indicates whether the flag should be force-enabled for testing purposes.
*/
boolean isForceEnabledForTest(@NonNull String flag);
+
+
+ /**
+ * Get the int value of the flag for testing purposes.
+ */
+ int getIntValueForTest(@NonNull String flag, int defaultValue);
}
/**
@@ -121,6 +149,19 @@
}
/**
+ * Get the int value of the flag for testing purposes.
+ *
+ * @return the test int value, or given default value if it is unset or the OverrideProvider
+ * doesn't exist.
+ */
+ private int getIntValueForTest(@NonNull String flag, int defaultValue) {
+ if (mOverrideProvider == null) {
+ return defaultValue;
+ }
+ return mOverrideProvider.getIntValueForTest(flag, defaultValue);
+ }
+
+ /**
* Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
*/
public boolean isUnicastReplyEnabled() {
@@ -160,6 +201,23 @@
}
/**
+ * Indicates whether {@link #NSD_CACHED_SERVICES_REMOVAL} is enabled, including for testing.
+ */
+ public boolean isCachedServicesRemovalEnabled() {
+ return mIsCachedServicesRemovalEnabled
+ || isForceEnabledForTest(NSD_CACHED_SERVICES_REMOVAL);
+ }
+
+ /**
+ * Get the value which is set to {@link #NSD_CACHED_SERVICES_RETENTION_TIME}, including for
+ * testing.
+ */
+ public long getCachedServicesRetentionTime() {
+ return getIntValueForTest(
+ NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -171,6 +229,8 @@
boolean isAggressiveQueryModeEnabled,
boolean isQueryWithKnownAnswerEnabled,
boolean avoidAdvertisingEmptyTxtRecords,
+ boolean isCachedServicesRemovalEnabled,
+ long cachedServicesRetentionTime,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -181,6 +241,8 @@
mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
mOverrideProvider = overrideProvider;
}
@@ -202,6 +264,8 @@
private boolean mIsAggressiveQueryModeEnabled;
private boolean mIsQueryWithKnownAnswerEnabled;
private boolean mAvoidAdvertisingEmptyTxtRecords;
+ private boolean mIsCachedServicesRemovalEnabled;
+ private long mCachedServicesRetentionTime;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -217,6 +281,8 @@
mIsAggressiveQueryModeEnabled = false;
mIsQueryWithKnownAnswerEnabled = false;
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
+ mIsCachedServicesRemovalEnabled = false;
+ mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
mOverrideProvider = null;
}
@@ -323,6 +389,26 @@
}
/**
+ * Set whether the cached services removal is enabled.
+ *
+ * @see #NSD_CACHED_SERVICES_REMOVAL
+ */
+ public Builder setIsCachedServicesRemovalEnabled(boolean isCachedServicesRemovalEnabled) {
+ mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
+ return this;
+ }
+
+ /**
+ * Set cached services retention time.
+ *
+ * @see #NSD_CACHED_SERVICES_RETENTION_TIME
+ */
+ public Builder setCachedServicesRetentionTime(long cachedServicesRetentionTime) {
+ mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -335,6 +421,8 @@
mIsAggressiveQueryModeEnabled,
mIsQueryWithKnownAnswerEnabled,
mAvoidAdvertisingEmptyTxtRecords,
+ mIsCachedServicesRemovalEnabled,
+ mCachedServicesRetentionTime,
mOverrideProvider);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index 591ed8b..22f7a03 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -49,7 +49,7 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- static class CacheKey {
+ public static class CacheKey {
@NonNull final String mUpperCaseServiceType;
@NonNull final SocketKey mSocketKey;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 4b55ea9..a5dd536 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -456,6 +456,14 @@
return executor;
}
+ /**
+ * Get the cache key for this service type client.
+ */
+ @NonNull
+ public MdnsServiceCache.CacheKey getCacheKey() {
+ return cacheKey;
+ }
+
private void removeScheduledTask() {
dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
sharedLog.log("Remove EVENT_START_QUERYTASK"
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 9b7af49..294a85a 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -51,12 +51,8 @@
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES;
-import static android.net.TrafficStats.TYPE_RX_BYTES;
-import static android.net.TrafficStats.TYPE_RX_PACKETS;
-import static android.net.TrafficStats.TYPE_TX_BYTES;
-import static android.net.TrafficStats.TYPE_TX_PACKETS;
import static android.net.TrafficStats.UID_TETHERING;
-import static android.net.TrafficStats.UNSUPPORTED;
+import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
@@ -308,9 +304,10 @@
static final String TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME =
"trafficstats_cache_expiry_duration_ms";
- static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
+ static final String TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME =
+ "trafficstats_cache_max_entries";
static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
- static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+ static final int DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES = 400;
/**
* The delay time between to network stats update intents.
* Added to fix intent spams (b/343844995)
@@ -491,13 +488,13 @@
private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
- static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
+ static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
"trafficstats_rate_limit_cache_enabled_flag";
static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
"broadcast_network_stats_updated_rate_limit_enabled_flag";
- private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+ private final boolean mAlwaysUseTrafficStatsServiceRateLimitCache;
private final int mTrafficStatsRateLimitCacheExpiryDuration;
- private final int mTrafficStatsRateLimitCacheMaxEntries;
+ private final int mTrafficStatsServiceRateLimitCacheMaxEntries;
private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
@@ -691,20 +688,23 @@
mEventLogger = null;
}
- mAlwaysUseTrafficStatsRateLimitCache =
- mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+ mAlwaysUseTrafficStatsServiceRateLimitCache =
+ mDeps.alwaysUseTrafficStatsServiceRateLimitCache(mContext);
mBroadcastNetworkStatsUpdatedRateLimitEnabled =
mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
mTrafficStatsRateLimitCacheExpiryDuration =
mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
- mTrafficStatsRateLimitCacheMaxEntries =
- mDeps.getTrafficStatsRateLimitCacheMaxEntries();
+ mTrafficStatsServiceRateLimitCacheMaxEntries =
+ mDeps.getTrafficStatsServiceRateLimitCacheMaxEntries();
mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
- mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+ mTrafficStatsRateLimitCacheExpiryDuration,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -964,14 +964,14 @@
}
/**
- * Get whether TrafficStats rate-limit cache is always applied.
+ * Get whether TrafficStats service side rate-limit cache is always applied.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
+ public boolean alwaysUseTrafficStatsServiceRateLimitCache(@NonNull Context ctx) {
return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
- ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
+ ctx, TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG);
}
/**
@@ -987,15 +987,15 @@
}
/**
- * Get TrafficStats rate-limit cache max entries.
+ * Get TrafficStats service side rate-limit cache max entries.
*
* This method should only be called once in the constructor,
* to ensure that the code does not need to deal with flag values changing at runtime.
*/
- public int getTrafficStatsRateLimitCacheMaxEntries() {
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
return getDeviceConfigPropertyInt(
- NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
- DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES);
+ NAMESPACE_TETHERING, TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES);
}
/**
@@ -2135,20 +2135,28 @@
@Override
public long getUidStats(int uid, int type) {
+ return getValueForTypeFromFirstEntry(getTypelessUidStats(uid), type);
+ }
+
+ @NonNull
+ @Override
+ public NetworkStats getTypelessUidStats(int uid) {
+ final NetworkStats stats = new NetworkStats(0, 0);
final int callingUid = Binder.getCallingUid();
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
- return UNSUPPORTED;
+ return stats;
}
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
-
- if (mAlwaysUseTrafficStatsRateLimitCache
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
- final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+ entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
() -> mDeps.nativeGetUidStat(uid));
- return getEntryValueForType(entry, type);
- }
+ } else entry = mDeps.nativeGetUidStat(uid);
- return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+ if (entry != null) {
+ stats.insertEntry(entry);
+ }
+ return stats;
}
@Nullable
@@ -2165,50 +2173,24 @@
return entry;
}
+ @NonNull
@Override
- public long getIfaceStats(@NonNull String iface, int type) {
+ public NetworkStats getTypelessIfaceStats(@NonNull String iface) {
Objects.requireNonNull(iface);
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(
ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+ entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
() -> getIfaceStatsInternal(iface));
- return getEntryValueForType(entry, type);
- }
+ } else entry = getIfaceStatsInternal(iface);
- return getEntryValueForType(getIfaceStatsInternal(iface), type);
- }
-
- private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
- if (entry == null) return UNSUPPORTED;
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- switch (type) {
- case TYPE_RX_BYTES:
- return entry.rxBytes;
- case TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TYPE_TX_BYTES:
- return entry.txBytes;
- case TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- throw new IllegalStateException("Bug: Invalid type: "
- + type + " should not reach here.");
+ NetworkStats stats = new NetworkStats(0, 0);
+ if (entry != null) {
+ stats.insertEntry(entry);
}
- }
-
- private boolean isEntryValueTypeValid(int type) {
- switch (type) {
- case TYPE_RX_BYTES:
- case TYPE_RX_PACKETS:
- case TYPE_TX_BYTES:
- case TYPE_TX_PACKETS:
- return true;
- default :
- return false;
- }
+ return stats;
}
@Nullable
@@ -2221,18 +2203,22 @@
return entry;
}
+ @NonNull
@Override
- public long getTotalStats(int type) {
- if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
- if (mAlwaysUseTrafficStatsRateLimitCache
+ public NetworkStats getTypelessTotalStats() {
+ final NetworkStats.Entry entry;
+ if (mAlwaysUseTrafficStatsServiceRateLimitCache
|| mDeps.isChangeEnabled(
ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
- final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+ entry = mTrafficStatsTotalCache.getOrCompute(
IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
- return getEntryValueForType(entry, type);
- }
+ } else entry = getTotalStatsInternal();
- return getEntryValueForType(getTotalStatsInternal(), type);
+ final NetworkStats stats = new NetworkStats(0, 0);
+ if (entry != null) {
+ stats.insertEntry(entry);
+ }
+ return stats;
}
@Override
@@ -3010,12 +2996,14 @@
} catch (IOException e) {
pw.println("(failed to dump FastDataInput counters)");
}
- pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
+ pw.print("trafficstats.service.cache.alwaysuse",
+ mAlwaysUseTrafficStatsServiceRateLimitCache);
pw.println();
pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
mTrafficStatsRateLimitCacheExpiryDuration);
pw.println();
- pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
+ pw.print(TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES_NAME,
+ mTrafficStatsServiceRateLimitCacheMaxEntries);
pw.println();
pw.decreaseIndent();
diff --git a/service/Android.bp b/service/Android.bp
index c68f0b8..94061a4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -161,7 +161,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
// so any calls to it must be protected with a check to ensure that it is
@@ -175,12 +175,12 @@
// TODO: figure out why just using "framework-tethering" uses the stubs, even though both
// service-connectivity and framework-tethering are in the same APEX.
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"unsupportedappusage",
"ServiceConnectivityResources",
- "framework-statsd",
- "framework-permission",
- "framework-permission-s",
+ "framework-statsd.stubs.module_lib",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
static_libs: [
// Do not add libs here if they are already included
@@ -264,10 +264,10 @@
"framework-connectivity.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"libprotobuf-java-nano",
- "framework-permission",
- "framework-permission-s",
+ "framework-permission.stubs.module_lib",
+ "framework-permission-s.stubs.module_lib",
],
jarjar_rules: ":connectivity-jarjar-rules",
apex_available: [
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
index 92dd9a1..8cefec4 100644
--- a/service/ServiceConnectivityResources/res/values-ar/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -40,7 +40,7 @@
<item msgid="5624324321165953608">"Wi-Fi"</item>
<item msgid="5667906231066981731">"بلوتوث"</item>
<item msgid="346574747471703768">"إيثرنت"</item>
- <item msgid="5734728378097476003">"شبكة افتراضية خاصة (VPN)"</item>
+ <item msgid="5734728378097476003">"شبكة VPN"</item>
</string-array>
<string name="network_switch_type_name_unknown" msgid="5116448402191972082">"نوع شبكة غير معروف"</string>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 81d8ddb..5a0a9d4 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Sistemaren konexio-baliabideak"</string>
- <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa Wi-Fi sarean"</string>
+ <string name="wifi_available_sign_in" msgid="8041178343789805553">"Hasi saioa wifi-sarean"</string>
<string name="network_available_sign_in" msgid="2622520134876355561">"Hasi saioa sarean"</string>
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml
index 02c60df..09f1255 100644
--- a/service/ServiceConnectivityResources/res/values-fa/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml
@@ -23,15 +23,15 @@
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
<string name="mobile_network_available_no_internet" msgid="1000871587359324217">"اتصال اینترنت وجود ندارد"</string>
- <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ممکن است داده <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> تمام شده باشد. برای گزینهها ضربه بزنید."</string>
- <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ممکن است داده شما تمام شده باشد. برای گزینهها ضربه بزنید."</string>
+ <string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"ممکن است داده <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> تمام شده باشد. برای گزینهها تکضرب بزنید."</string>
+ <string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"ممکن است داده شما تمام شده باشد. برای گزینهها تکضرب بزنید."</string>
<string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
- <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینهها ضربه بزنید"</string>
+ <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"برای گزینهها تکضرب بزنید"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"شبکه به اینترنت دسترسی ندارد"</string>
<string name="private_dns_broken_detailed" msgid="2677123850463207823">"سرور DNS خصوصی قابل دسترسی نیست"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال محدودی دارد"</string>
- <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"بههرصورت، برای اتصال ضربه بزنید"</string>
+ <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"بههرصورت، برای اتصال تکضرب بزنید"</string>
<string name="network_switch_metered" msgid="5016937523571166319">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string>
<string name="network_switch_metered_detail" msgid="1257300152739542096">"وقتی <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> به اینترنت دسترسی نداشته باشد، دستگاه از <xliff:g id="NEW_NETWORK">%1$s</xliff:g> استفاده میکند. ممکن است هزینههایی اعمال شود."</string>
<string name="network_switch_metered_toast" msgid="70691146054130335">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string>
diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml
index 08ffd2a..398531a 100644
--- a/service/ServiceConnectivityResources/res/values-ky/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml
@@ -26,7 +26,7 @@
<string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"<xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> трафиги түгөнгөн окшойт. Параметрлерди ачуу үчүн таптаңыз."</string>
<string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Трафик түгөнгөн окшойт. Параметрлерди ачуу үчүн таптаңыз."</string>
<string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташуусу жок"</string>
- <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Параметрлерди ачуу үчүн таптап коюңуз"</string>
+ <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Параметрлерди ачуу үчүн тийип коюңуз"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"Мобилдик Интернет жок"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Тармактын Интернет жок"</string>
<string name="private_dns_broken_detailed" msgid="2677123850463207823">"Жеке DNS сервери жеткиликсиз"</string>
diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml
index 2f13ef4..9af035b 100644
--- a/service/ServiceConnectivityResources/res/values-mn/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml
@@ -27,7 +27,7 @@
<string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Таны дата дууссан байж магадгүй. Сонголтыг харахын тулд товшино уу."</string>
<string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернэтийн хандалт алга"</string>
<string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Сонголт хийхийн тулд товшино уу"</string>
- <string name="mobile_no_internet" msgid="4087718456753201450">"Мобайл сүлжээнд интернэт хандалт байхгүй байна"</string>
+ <string name="mobile_no_internet" msgid="4087718456753201450">"Хөдөлгөөнт холбооны сүлжээнд интернэт хандалт байхгүй байна"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Сүлжээнд интернэт хандалт байхгүй байна"</string>
<string name="private_dns_broken_detailed" msgid="2677123850463207823">"Хувийн DNS серверт хандах боломжгүй байна"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгээнд хандах боломжгүй байна"</string>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 9af250b..cb62ae1 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -407,6 +407,7 @@
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@@ -8932,9 +8933,15 @@
@NonNull
final NetworkRequestInfo mDefaultRequest;
// Collection of NetworkRequestInfo's used for default networks.
+ // This set is read and iterated on multiple threads.
+ // Using CopyOnWriteArraySet since number of default network request is small (system default
+ // network request + per-app default network requests) and updated infrequently but read
+ // frequently.
@VisibleForTesting
@NonNull
- final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>();
+ final CopyOnWriteArraySet<NetworkRequestInfo> mDefaultNetworkRequests =
+ new CopyOnWriteArraySet<>();
+
private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) {
return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri);
@@ -14505,4 +14512,14 @@
}
return features;
}
+
+ @Override
+ public boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag) {
+ switch (featureFlag) {
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return mIngressToVpnAddressFiltering;
+ default:
+ throw new IllegalArgumentException("Unknown flag: " + featureFlag);
+ }
+ }
}
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index 917ad4d..7a008c6 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -43,7 +43,7 @@
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
private static final String BLOCKED_PORTS_MAP_PATH =
- "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
+ "/sys/fs/bpf/netd_shared/map_netd_blocked_ports_map";
private final Context mContext;
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index 7162a4a..a9100ac 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -117,8 +117,8 @@
this.proto = proto != -1 ? proto : 0;
this.dscp = dscp;
- this.match_src_ip = (this.src46 != EMPTY_ADDRESS_FIELD);
- this.match_dst_ip = (this.dst46 != EMPTY_ADDRESS_FIELD);
+ this.match_src_ip = (src46 != null);
+ this.match_dst_ip = (dst46 != null);
this.match_src_port = (srcPort != -1);
this.match_proto = (proto != -1);
}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index f47a23a..85258f8 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -70,7 +70,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
],
lint: {
@@ -264,7 +264,7 @@
],
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
],
static_libs: [
"net-utils-device-common",
@@ -342,7 +342,7 @@
min_sdk_version: "30",
libs: [
"framework-annotations-lib",
- "framework-connectivity",
+ "framework-connectivity.stubs.module_lib",
"modules-utils-build_system",
],
// TODO: remove "apex_available:platform".
@@ -468,7 +468,7 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
],
lint: {
@@ -484,12 +484,11 @@
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
- "framework-configinfrastructure",
- "framework-connectivity",
+ "framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-connectivity-t.stubs.module_lib",
"framework-location.stubs.module_lib",
- "framework-tethering",
+ "framework-tethering.stubs.module_lib",
"unsupportedappusage",
],
static_libs: [
@@ -522,6 +521,8 @@
],
libs: [
"net-utils-framework-connectivity",
+ "framework-connectivity.impl",
+ "framework-tethering.impl",
],
defaults: ["net-utils-non-bootclasspath-defaults"],
jarjar_rules: "jarjar-rules-shared.txt",
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 7aafd69..79234f5 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -17,8 +17,8 @@
"netd-client",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs.system",
+ "android.test.base.stubs.system",
],
visibility: [
// Visible for Tethering and NetworkStack integration test and link NetdStaticLibTestsLib
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index 28c33f3..e4d25cd 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -117,7 +117,11 @@
@VisibleForTesting(visibility = PRIVATE)
public @LocationPermissionCheckStatus int checkLocationPermissionInternal(
String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
- checkPackage(uid, pkgName);
+ try {
+ checkPackage(uid, pkgName);
+ } catch (SecurityException e) {
+ return ERROR_LOCATION_PERMISSION_MISSING;
+ }
// Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
// are granted a bypass.
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 61f41f7..8c54e6a 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -30,8 +30,8 @@
"net-utils-service-connectivity",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
visibility: [
"//frameworks/base/packages/Tethering/tests/integration",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
index c8f8656..d773374 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -18,7 +18,6 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -47,7 +46,6 @@
import com.android.testutils.DevSdkIgnoreRule;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -242,9 +240,9 @@
mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
setupTestCase();
- assertThrows(SecurityException.class,
- () -> mChecker.checkLocationPermissionInternal(
- TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+ final int result = mChecker.checkLocationPermissionInternal(
+ TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+ assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
}
@Test
@@ -305,14 +303,4 @@
TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
assertEquals(LocationPermissionChecker.SUCCEEDED, result);
}
-
-
- private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
- try {
- r.run();
- Assert.fail("Expected " + exceptionClass + " to be thrown.");
- } catch (Exception exception) {
- assertTrue(exceptionClass.isInstance(exception));
- }
- }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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.testutils;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+ private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+ /**
+ * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+ * `records`. Each record must contain the service priority, the target name, and the service
+ * parameters.
+ * E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+ */
+ @NonNull
+ public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+ if (records == null) throw new NullPointerException();
+ if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ // Write DNS header.
+ os.write(shortsToByteArray(
+ 0x1234, /* Transaction ID */
+ 0x8100, /* Flags */
+ 1, /* qdcount */
+ records.length, /* ancount */
+ 0, /* nscount */
+ 0 /* arcount */
+ ));
+ // Write Question.
+ // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+ // the writing into two steps.
+ os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+ os.write(domainNameToLabels(hostname.substring(5)));
+ os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+ // Write Answer section.
+ for (String r : records) {
+ os.write(makeSvcbRecord(r));
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] makeSvcbRecord(String representation) throws IOException {
+ if (representation == null) return new byte[0];
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(shortsToByteArray(
+ 0xc00c, /* Pointer to qname in question section */
+ TYPE_SVCB,
+ CLASS_IN,
+ 0, 16, /* TTL = 16 */
+ 0 /* Data Length = 0 */
+
+ ));
+ final String[] strings = representation.split(" +");
+ // SvcPriority and TargetName are mandatory in the representation.
+ if (strings.length < 3) {
+ fail("Invalid SVCB representation: " + representation);
+ }
+ // Write SvcPriority, TargetName, and SvcParams.
+ os.write(shortsToByteArray(Short.parseShort(strings[0])));
+ os.write(domainNameToLabels(strings[1]));
+ for (int i = 2; i < strings.length; i++) {
+ try {
+ os.write(svcParamToByteArray(strings[i]));
+ } catch (UnsupportedEncodingException e) {
+ throw new IOException(e);
+ }
+ }
+ // Update rdata length.
+ final byte[] out = os.toByteArray();
+ ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+ return out;
+ }
+
+ @NonNull
+ private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+ final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+ if (!matcher.matches() || matcher.groupCount() != 2) {
+ fail("Invalid SvcParam: " + svcParam);
+ }
+ final String svcParamkey = matcher.group(1);
+ final String svcParamValue = matcher.group(2);
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(svcParamKeyToBytes(svcParamkey));
+ switch (svcParamkey) {
+ case "mandatory":
+ final String[] keys = svcParamValue.split(",");
+ os.write(shortsToByteArray(keys.length));
+ for (String v : keys) {
+ os.write(svcParamKeyToBytes(v));
+ }
+ break;
+ case "alpn":
+ os.write(shortsToByteArray((svcParamValue.length() + 1)));
+ for (String v : svcParamValue.split(",")) {
+ os.write(v.length());
+ // TODO: support percent-encoding per RFC 7838.
+ os.write(v.getBytes(US_ASCII));
+ }
+ break;
+ case "no-default-alpn":
+ os.write(shortsToByteArray(0));
+ break;
+ case "port":
+ os.write(shortsToByteArray(2));
+ os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+ break;
+ case "ipv4hint":
+ final String[] v4Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+ for (String v : v4Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "ech":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII)); // base64 encoded
+ break;
+ case "ipv6hint":
+ final String[] v6Addrs = svcParamValue.split(",");
+ os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+ for (String v : v6Addrs) {
+ os.write(InetAddresses.parseNumericAddress(v).getAddress());
+ }
+ break;
+ case "dohpath":
+ os.write(shortsToByteArray(svcParamValue.length()));
+ // TODO: support percent-encoding, since this is a URI template.
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ default:
+ os.write(shortsToByteArray(svcParamValue.length()));
+ os.write(svcParamValue.getBytes(US_ASCII));
+ break;
+ }
+ return os.toByteArray();
+ }
+
+ @NonNull
+ private static byte[] svcParamKeyToBytes(String key) {
+ switch (key) {
+ case "mandatory": return shortsToByteArray(0);
+ case "alpn": return shortsToByteArray(1);
+ case "no-default-alpn": return shortsToByteArray(2);
+ case "port": return shortsToByteArray(3);
+ case "ipv4hint": return shortsToByteArray(4);
+ case "ech": return shortsToByteArray(5);
+ case "ipv6hint": return shortsToByteArray(6);
+ case "dohpath": return shortsToByteArray(7);
+ default:
+ if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+ return shortsToByteArray(Short.parseShort(key.substring(3)));
+ }
+ }
+
+ @NonNull
+ private static byte[] shortsToByteArray(int... values) {
+ final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+ for (int value: values) {
+ if (value < 0 || value > 0xffff) {
+ throw new AssertionError("not an unsigned short: " + value);
+ }
+ out.putShort((short) value);
+ }
+ return out.array();
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
index 1f82a35..e49c0c7 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -18,72 +18,56 @@
import android.net.DnsResolver
import android.net.InetAddresses
-import android.os.Looper
+import android.net.Network
import android.os.Handler
+import android.os.Looper
import com.android.internal.annotations.GuardedBy
-import java.net.InetAddress
-import java.util.concurrent.Executor
-import org.mockito.invocation.InvocationOnMock
+import com.android.net.module.util.DnsPacket
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doAnswer
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import java.net.InetAddress
+import java.net.UnknownHostException
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
-const val TYPE_UNSPECIFIED = -1
-// TODO: Integrate with NetworkMonitorTest.
-class FakeDns(val mockResolver: DnsResolver) {
- class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
- fun match(host: String, type: Int) = hostname.equals(host) && type == type
- }
+// Nonexistent DNS query type to represent "A and/or AAAA queries".
+// TODO: deduplicate this with DnsUtils.TYPE_ADDRCONFIG.
+private const val TYPE_ADDRCONFIG = -1
- @GuardedBy("answers")
- val answers = ArrayList<DnsEntry>()
+class FakeDns(val network: Network, val dnsResolver: DnsResolver) {
+ private val HANDLER_TIMEOUT_MS = 1000
- fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
- return answers.firstOrNull { it.match(hostname, type) }
- }
-
- fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
- val ans = DnsEntry(hostname, type, generateAnswer(answer))
- // Replace or remove the existing one.
- when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
- -1 -> answers.add(ans)
- else -> answers[index] = ans
+ /** Data class to record the Dns entry. */
+ class DnsEntry (val hostname: String, val type: Int, val answerSupplier: AnswerSupplier) {
+ // Full match or partial match that target host contains the entry hostname to support
+ // random private dns probe hostname.
+ fun matches(hostname: String, type: Int): Boolean {
+ return hostname.endsWith(this.hostname) && type == this.type
}
}
- private fun generateAnswer(answer: Array<String>) =
- answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
+ /**
+ * Whether queries on [network] will be answered when private DNS is enabled. Queries that
+ * bypass private DNS by using [network.privateDnsBypassingCopy] are always answered.
+ */
+ var nonBypassPrivateDnsWorking: Boolean = true
- fun startMocking() {
- // Mock DnsResolver.query() w/o type
- doAnswer {
- mockAnswer(it, 1, -1, 3, 5)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
- any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
- // Mock DnsResolver.query() w/ type
- doAnswer {
- mockAnswer(it, 1, 2, 4, 6)
- }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
- anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
- any() /*callback*/)
+ @GuardedBy("answers")
+ private val answers = mutableListOf<DnsEntry>()
+
+ interface AnswerSupplier {
+ /** Supplies the answer to one DnsResolver query method call. */
+ @Throws(DnsResolver.DnsException::class)
+ fun get(): Array<String>?
}
- private fun mockAnswer(
- it: InvocationOnMock,
- posHos: Int,
- posType: Int,
- posExecutor: Int,
- posCallback: Int
- ) {
- val hostname = it.arguments[posHos] as String
- val executor = it.arguments[posExecutor] as Executor
- val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
- var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
- val answer = getAnswer(hostname, type)
-
- if (answer != null && !answer.addresses.isNullOrEmpty()) {
- Handler(Looper.getMainLooper()).post({ executor.execute({
- callback.onAnswer(answer.addresses, 0); }) })
+ private class InstantAnswerSupplier(val answers: Array<String>?) : AnswerSupplier {
+ override fun get(): Array<String>? {
+ return answers
}
}
@@ -91,4 +75,177 @@
fun clearAll() = synchronized(answers) {
answers.clear()
}
+
+ /** Returns the answer for a given name and type on the given mock network. */
+ private fun getAnswer(mockNetwork: Network, hostname: String, type: Int):
+ CompletableFuture<Array<String>?> {
+ if (!checkQueryNetwork(mockNetwork)) {
+ return CompletableFuture.completedFuture(null)
+ }
+ val answerSupplier: AnswerSupplier? = synchronized(answers) {
+ answers.firstOrNull({e: DnsEntry -> e.matches(hostname, type)})?.answerSupplier
+ }
+ if (answerSupplier == null) {
+ return CompletableFuture.completedFuture(null)
+ }
+ if (answerSupplier is InstantAnswerSupplier) {
+ // Save latency waiting for a query thread if the answer is hardcoded.
+ return CompletableFuture.completedFuture<Array<String>?>(answerSupplier.get())
+ }
+ val answerFuture = CompletableFuture<Array<String>?>()
+ // Don't worry about ThreadLeadMonitor: these threads terminate immediately, so they won't
+ // leak, and ThreadLeakMonitor won't monitor them anyway, since they have one-time names
+ // such as "Thread-42".
+ Thread {
+ try {
+ answerFuture.complete(answerSupplier.get())
+ } catch (e: DnsResolver.DnsException) {
+ answerFuture.completeExceptionally(e)
+ }
+ }.start()
+ return answerFuture
+ }
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(hostname: String, answer: Array<String>?, type: Int) = setAnswer(
+ hostname, InstantAnswerSupplier(answer), type)
+
+ /** Sets the answer for a given name and type. */
+ fun setAnswer(
+ hostname: String, answerSupplier: AnswerSupplier, type: Int) = synchronized (answers) {
+ val ans = DnsEntry(hostname, type, answerSupplier)
+ // Replace or remove the existing one.
+ when (val index = answers.indexOfFirst { it.matches(hostname, type) }) {
+ -1 -> answers.add(ans)
+ else -> answers[index] = ans
+ }
+ }
+
+ private fun checkQueryNetwork(mockNetwork: Network): Boolean {
+ // Queries on the wrong network do not work.
+ // Queries that bypass private DNS work.
+ // Queries that do not bypass private DNS work only if nonBypassPrivateDnsWorking is true.
+ return mockNetwork == network.privateDnsBypassingCopy ||
+ mockNetwork == network && nonBypassPrivateDnsWorking
+ }
+
+ /** Simulates a getAllByName call for the specified name on the specified mock network. */
+ private fun getAllByName(mockNetwork: Network, hostname: String): Array<InetAddress>? {
+ val answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname)
+ .get(HANDLER_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS))
+ if (answer == null || answer.size == 0) {
+ throw UnknownHostException(hostname)
+ }
+ return answer.toTypedArray()
+ }
+
+ // Regardless of the type, depends on what the responses contained in the network.
+ private fun queryAllTypes(
+ mockNetwork: Network, hostname: String
+ ): CompletableFuture<Array<String>?> {
+ val aFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_A)
+ .exceptionally { emptyArray() }
+ val aaaaFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_AAAA)
+ .exceptionally { emptyArray() }
+ val combinedFuture = CompletableFuture<Array<String>?>()
+ aFuture.thenAcceptBoth(aaaaFuture) { res1: Array<String>?, res2: Array<String>? ->
+ var answer: Array<String> = arrayOf()
+ if (res1 != null) answer += res1
+ if (res2 != null) answer += res2
+ combinedFuture.complete(answer)
+ }
+ return combinedFuture
+ }
+
+ /** Starts mocking DNS queries. */
+ fun startMocking() {
+ // Queries on mNetwork using getAllByName.
+ doAnswer {
+ getAllByName(it.mock as Network, it.getArgument(0))
+ }.`when`(network).getAllByName(any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 3, posCallback = 5,
+ posType = -1)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), any(), any(), any())
+
+ // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 4, posCallback = 6,
+ posType = 2)
+ }.`when`(dnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any())
+
+ // Queries using rawQuery. Currently, mockQuery only supports TYPE_SVCB.
+ doAnswer {
+ mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 5, posCallback = 7,
+ posType = 3)
+ }.`when`(dnsResolver).rawQuery(any(), any(), anyInt(), anyInt(), anyInt(), any(), any(),
+ any())
+ }
+
+ private fun stringsToInetAddresses(addrs: Array<String>?): List<InetAddress>? {
+ if (addrs == null) return null
+ val out: MutableList<InetAddress> = ArrayList()
+ for (addr in addrs) {
+ out.add(InetAddresses.parseNumericAddress(addr))
+ }
+ return out
+ }
+
+ // Mocks all the DnsResolver query methods used in this test.
+ private fun mockQuery(
+ invocation: InvocationOnMock, posNetwork: Int, posHostname: Int,
+ posExecutor: Int, posCallback: Int, posType: Int
+ ): Answer<*>? {
+ val hostname = invocation.getArgument<String>(posHostname)
+ val executor = invocation.getArgument<Executor>(posExecutor)
+ val network = invocation.getArgument<Network>(posNetwork)
+ val qtype = if (posType != -1) invocation.getArgument(posType) else TYPE_ADDRCONFIG
+ val answerFuture: CompletableFuture<Array<String>?> = if (posType != -1) getAnswer(
+ network,
+ hostname,
+ invocation.getArgument(posType)
+ ) else queryAllTypes(network, hostname)
+
+ // Discriminate between different callback types to avoid unchecked cast warnings when
+ // calling the onAnswer methods.
+ val inetAddressCallback: DnsResolver.Callback<List<InetAddress>> =
+ invocation.getArgument(posCallback)
+ val byteArrayCallback: DnsResolver.Callback<ByteArray> =
+ invocation.getArgument(posCallback)
+ val callback: DnsResolver.Callback<*> = invocation.getArgument(posCallback)
+
+ answerFuture.whenComplete { answer: Array<String>?, exception: Throwable? ->
+ // Use getMainLooper() because that's what android.net.DnsResolver currently uses.
+ Handler(Looper.getMainLooper()).post {
+ executor.execute {
+ if (exception != null) {
+ if (exception !is DnsResolver.DnsException) {
+ throw java.lang.AssertionError(
+ "Test error building DNS response",
+ exception
+ )
+ }
+ callback.onError((exception as DnsResolver.DnsException?)!!)
+ return@execute
+ }
+ if (answer != null && answer.size > 0) {
+ when (qtype) {
+ DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA, TYPE_ADDRCONFIG ->
+ inetAddressCallback.onAnswer(stringsToInetAddresses(answer)!!, 0)
+ DnsPacket.TYPE_SVCB ->
+ byteArrayCallback.onAnswer(
+ DnsSvcbUtils.makeSvcbResponse(hostname, answer), 0)
+ else -> throw UnsupportedOperationException(
+ "Unsupported qtype $qtype, update this fake"
+ )
+ }
+ }
+ }
+ }
+ }
+ // If the future does not complete or has no answer do nothing. The timeout should fire.
+ return null
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 70f20d6..58e6622 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -65,10 +65,11 @@
@Override
public void insertEntry(K key, V value) throws ErrnoException,
- IllegalArgumentException {
- // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+ IllegalStateException {
+ // The entry is created if and only if it doesn't exist.
+ // And throws exception if it exists. See BpfMap#insertEntry.
if (mMap.get(key) != null) {
- throw new IllegalArgumentException(key + " already exist");
+ throw new IllegalStateException(key + " already exist");
}
mMap.put(key, value);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
index 84fb47b..341d55f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -29,7 +29,6 @@
import android.os.Binder
import android.os.Build
import androidx.annotation.RequiresApi
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -137,7 +136,6 @@
network = try {
if (lp != null) {
- assertTrue(isAtLeastR(), "Cannot specify TestNetwork LinkProperties before R")
tnm.setupTestNetwork(lp, true /* isMetered */, binder)
} else {
tnm.setupTestNetwork(iface.interfaceName, binder)
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
index 7203265..9a30978 100644
--- a/staticlibs/testutils/host/python/apf_test_base.py
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -23,6 +23,11 @@
super().setup_class()
# Check test preconditions.
+ asserts.abort_class_if(
+ not self.client.isAtLeastV(),
+ "Do not enforce the test until V+ since chipset potential bugs are"
+ " expected to be fixed on V+ releases.",
+ )
tether_utils.assume_hotspot_test_preconditions(
self.serverDevice, self.clientDevice, UpstreamType.NONE
)
@@ -34,13 +39,12 @@
)
# Fetch device properties and storing them locally for later use.
- client = self.clientDevice.connectivity_multi_devices_snippet
self.server_iface_name, client_network = (
tether_utils.setup_hotspot_and_client_for_upstream_type(
self.serverDevice, self.clientDevice, UpstreamType.NONE
)
)
- self.client_iface_name = client.getInterfaceNameFromNetworkHandle(
+ self.client_iface_name = self.client.getInterfaceNameFromNetworkHandle(
client_network
)
self.server_mac_address = apf_utils.get_hardware_address(
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index a3ec6e9..c3330d2 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -236,7 +236,7 @@
ad: android_device.AndroidDevice, iface_name: str, expected_version: int
) -> None:
caps = get_apf_capabilities(ad, iface_name)
- asserts.skip_if(
+ asserts.abort_class_if(
caps.apf_version_supported < expected_version,
f"Supported apf version {caps.apf_version_supported} < expected version"
f" {expected_version}",
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
index f8a92f3..677329a 100644
--- a/staticlibs/testutils/host/python/multi_devices_test_base.py
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -52,3 +52,4 @@
max_workers=2,
raise_on_exception=True,
)
+ self.client = self.clientDevice.connectivity_multi_devices_snippet
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 33761dc..31924f0 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -20,6 +20,7 @@
java_test_helper_library {
name: "CtsHostsideNetworkTestsAidl",
sdk_version: "current",
+ min_sdk_version: "30",
srcs: [
"com/android/cts/net/hostside/*.aidl",
"com/android/cts/net/hostside/*.java",
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 798cf98..2ca9adb 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -33,8 +33,8 @@
"modules-utils-build",
],
libs: [
- "android.test.runner",
- "android.test.base",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
],
srcs: [
"src/**/*.java",
@@ -44,7 +44,7 @@
"general-tests",
"sts",
],
- min_sdk_version: "31",
+ min_sdk_version: "30",
}
android_test_helper_app {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index e186c6b..d7631eb 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -50,7 +50,6 @@
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.RecorderCallback.CallbackEntry.BLOCKED_STATUS_INT;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -213,6 +212,8 @@
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
"automatic_on_off_keepalive_version";
+ private static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
// Enabled since version 1 means it's always enabled because the version is always above 1
private static final String AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED = "1";
private static final long TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS = 60_000L;
@@ -890,7 +891,7 @@
entry -> entry.getCaps().hasTransport(TRANSPORT_VPN));
}
- @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testChangeUnderlyingNetworks() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
@@ -995,6 +996,13 @@
FIREWALL_CHAIN_BACKGROUND));
otherUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
true /* validated */, isOtherUidBlocked, TIMEOUT_MS);
+ } else {
+ // R does not have per-UID callback or system default callback APIs, and sends an
+ // additional CAP_CHANGED callback.
+ registerDefaultNetworkCallback(myUidCallback);
+ myUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+ true /* validated */, false /* blocked */, TIMEOUT_MS);
+ myUidCallback.expect(CallbackEntry.NETWORK_CAPS_UPDATED, defaultNetwork);
}
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1136,12 +1144,12 @@
return null;
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeNoClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(false);
}
- @Test
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Automatic keepalives were added in U.
public void testAutomaticOnOffKeepaliveModeClose() throws Exception {
doTestAutomaticOnOffKeepaliveMode(true);
}
@@ -1707,7 +1715,8 @@
}
private void maybeExpectVpnTransportInfo(Network network) {
- assumeTrue(SdkLevel.isAtLeastS());
+ // VpnTransportInfo was only added in S.
+ if (!SdkLevel.isAtLeastS()) return;
final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
final TransportInfo ti = vpnNc.getTransportInfo();
@@ -1949,6 +1958,9 @@
*/
private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
throws Exception {
+ assumeTrue(mCM.isConnectivityServiceFeatureEnabledForTesting(
+ INGRESS_TO_VPN_ADDRESS_FILTERING));
+
final NetworkRequest request = new NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 12ea23b..cb55c7b 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -36,4 +36,5 @@
"sts",
],
sdk_version: "test_current",
+ min_sdk_version: "30",
}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index c220000..79ad2e2 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -35,6 +35,7 @@
"general-tests",
"sts",
],
+ min_sdk_version: "30",
}
android_test_helper_app {
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 5f062f1..40aa1e4 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -26,6 +26,7 @@
"run_tests.py",
],
libs: [
+ "absl-py",
"mobly",
"net-tests-utils-host-python-common",
],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index 4633d37..7795be5 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -12,23 +12,52 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from net_tests_utils.host.python import apf_test_base
+from absl.testing import parameterized
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils
# Constants.
COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
-ETH_P_ETHERCAT = "88A4"
-class ApfV4Test(apf_test_base.ApfTestBase):
+class ApfV4Test(apf_test_base.ApfTestBase, parameterized.TestCase):
+ def setup_class(self):
+ super().setup_class()
+ # Check apf version preconditions.
+ caps = apf_utils.get_apf_capabilities(
+ self.clientDevice, self.client_iface_name
+ )
+ if self.client.getVsrApiLevel() >= 34:
+ # Enforce APFv4 support for Android 14+ VSR.
+ asserts.assert_true(
+ caps.apf_version_supported >= 4,
+ "APFv4 became mandatory in Android 14 VSR.",
+ )
+ else:
+ # Skip tests for APF version < 4 before Android 14 VSR.
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, 4
+ )
- def test_apf_drop_ethercat(self):
+ # APF L2 packet filtering on V+ Android allows only specific
+ # types: IPv4, ARP, IPv6, EAPOL, WAPI.
+ # Tests can use any disallowed packet type. Currently,
+ # several ethertypes from the legacy ApfFilter denylist are used.
+ @parameterized.parameters(
+ "88a2", # ATA over Ethernet
+ "88a4", # EtherCAT
+ "88b8", # GOOSE (Generic Object Oriented Substation event)
+ "88cd", # SERCOS III
+ "88e3", # Media Redundancy Protocol (IEC62439-2)
+ ) # Declare inputs for state_str and expected_result.
+ def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
# Ethernet header (14 bytes).
packet = ETHER_BROADCAST_ADDR # Destination MAC (broadcast)
packet += self.server_mac_address.replace(":", "") # Source MAC
- packet += ETH_P_ETHERCAT # EtherType (EtherCAT)
+ packet += blocked_ether_type
- # EtherCAT header (2 bytes) + 44 bytes of zero padding.
+ # Pad with zeroes to minimum ethernet frame length.
packet += "00" * 46
self.send_packet_and_expect_counter_increased(
packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 7368669..49688cc 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -36,6 +36,7 @@
import android.net.wifi.WifiNetworkSpecifier
import android.net.wifi.WifiSsid
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.ConnectUtil
@@ -75,6 +76,12 @@
@Rpc(description = "Check whether the device SDK is as least T")
fun isAtLeastT() = SdkLevel.isAtLeastT()
+ @Rpc(description = "Return whether the Sdk level is at least V.")
+ fun isAtLeastV() = SdkLevel.isAtLeastV()
+
+ @Rpc(description = "Return the API level that the VSR requirement must be fulfilled.")
+ fun getVsrApiLevel() = PropertyUtil.getVsrApiLevel()
+
@Rpc(description = "Request cellular connection and ensure it is the default network.")
fun requestCellularAndEnsureDefault() {
ctsNetUtils.disableWifi()
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 1cd8327..a5ad7f2 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -29,7 +29,7 @@
libs: [
"voip-common",
- "android.test.base",
+ "android.test.base.stubs",
],
jni_libs: [
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 587d5a5..7d93c3a 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -25,7 +25,7 @@
compile_multilib: "both",
libs: [
- "android.test.base",
+ "android.test.base.stubs.test",
],
srcs: [
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
index 8042d50..fcc73f3 100644
--- a/tests/cts/net/api23Test/AndroidTest.xml
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -18,6 +18,7 @@
<option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 07e2024..1389be7 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -44,7 +44,6 @@
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.AndroidJUnit4
-import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
import com.android.testutils.AutoReleaseNetworkCallbackRule
@@ -201,10 +200,7 @@
"access."
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
- val startPortalAppPermission =
- if (isAtLeastR()) NETWORK_SETTINGS
- else CONNECTIVITY_INTERNAL
- runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+ runAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
// Expect the portal content to be fetched at some point after detecting the portal.
// Some implementations may fetch the URL before startCaptivePortalApp is called.
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index f73134a..041e6cb 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -298,7 +298,8 @@
fun sendPacket(
agent: TestableNetworkAgent,
sendV6: Boolean,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
val testString = "test string"
val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
@@ -308,9 +309,11 @@
IPPROTO_UDP)
checkNotNull(agent.network).bindSocket(socket)
- val originalPacket = testPacket.readAsArray()
- Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+ val origPacket = testPacket.readAsArray()
+ repeat(times) {
+ Os.sendto(socket, origPacket, 0 /* bytesOffset */, origPacket.size, 0 /* flags */,
if (sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
+ }
Os.close(socket)
}
@@ -400,10 +403,11 @@
agent: TestableNetworkAgent,
sendV6: Boolean = false,
dscpValue: Int = 0,
- dstPort: Int = 0
+ dstPort: Int = 0,
+ times: Int = 1
) {
- var packetFound = false
- sendPacket(agent, sendV6, dstPort)
+ var packetFound = 0
+ sendPacket(agent, sendV6, dstPort, times)
// TODO: grab source port from socket in sendPacket
Log.e(TAG, "find DSCP value:" + dscpValue)
@@ -424,10 +428,23 @@
if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
Log.e(TAG, "DSCP value found")
assertEquals(dscpValue, dscp)
- packetFound = true
+ packetFound++
}
}
- assertTrue(packetFound)
+ assertTrue(packetFound == times)
+ }
+
+ fun validatePackets(
+ agent: TestableNetworkAgent,
+ sendV6: Boolean = false,
+ dscpValue: Int = 0,
+ dstPort: Int = 0
+ ) {
+ // We send two packets from the same socket to verify
+ // socket caching works correctly.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 2)
+ // Try one more time from a different socket.
+ validatePacket(agent, sendV6, dscpValue, dstPort, 1)
}
fun doRemovePolicyTest(
@@ -453,10 +470,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -475,7 +489,7 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -494,10 +508,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
- // Send a second packet to validate that the stored BPF policy
- // is correct for subsequent packets.
- validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, true, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -515,7 +526,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
+ validatePackets(agent, true, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -533,7 +544,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -541,7 +552,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -549,16 +560,16 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
doRemovePolicyTest(agent, callback, 1)
- validatePacket(agent, dscpValue = 0, dstPort = 1111)
+ validatePackets(agent, dscpValue = 0, dstPort = 1111)
doRemovePolicyTest(agent, callback, 2)
- validatePacket(agent, dscpValue = 0, dstPort = 2222)
+ validatePackets(agent, dscpValue = 0, dstPort = 2222)
doRemovePolicyTest(agent, callback, 3)
- validatePacket(agent, dscpValue = 0, dstPort = 3333)
+ validatePackets(agent, dscpValue = 0, dstPort = 3333)
}
@Test
@@ -569,7 +580,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
doRemovePolicyTest(agent, callback, 1)
@@ -578,7 +589,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
doRemovePolicyTest(agent, callback, 2)
@@ -587,7 +598,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
doRemovePolicyTest(agent, callback, 3)
}
@@ -601,7 +612,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -609,7 +620,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -617,7 +628,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
@@ -643,7 +654,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 1111)
+ validatePackets(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1)
@@ -652,7 +663,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 2222)
+ validatePackets(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1)
@@ -661,24 +672,24 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 3333)
+ validatePackets(agent, dscpValue = 1, dstPort = 3333)
}
agent.sendRemoveAllDscpPolicies()
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 1111)
+ validatePackets(agent, false, dstPort = 1111)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 2222)
+ validatePackets(agent, false, dstPort = 2222)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- validatePacket(agent, false, dstPort = 3333)
+ validatePackets(agent, false, dstPort = 3333)
}
}
@@ -690,7 +701,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- validatePacket(agent, dscpValue = 1, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 4444)
}
val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
@@ -700,8 +711,8 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
// Sending packet with old policy should fail
- validatePacket(agent, dscpValue = 0, dstPort = 4444)
- validatePacket(agent, dscpValue = 1, dstPort = 5555)
+ validatePackets(agent, dscpValue = 0, dstPort = 4444)
+ validatePackets(agent, dscpValue = 1, dstPort = 5555)
}
agent.sendRemoveDscpPolicy(1)
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 1023173..1165018 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -22,7 +22,7 @@
defaults: ["cts_defaults"],
libs: [
- "android.test.base",
+ "android.test.base.stubs.system",
],
srcs: [
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 349529dd..6c3b7a0 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -33,7 +33,7 @@
"src/**/*.aidl",
],
libs: [
- "android.test.mock",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
static_libs: [
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index ef3ebb0..00f9d05 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -104,9 +104,9 @@
],
libs: [
"android.net.ipsec.ike.stubs.module_lib",
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
"ServiceConnectivityResources",
],
exclude_kotlinc_generated_files: false,
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
index 0b82759..e8f91e6 100644
--- a/tests/unit/java/android/net/IpMemoryStoreTest.java
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -16,6 +16,11 @@
package android.net;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -68,6 +73,14 @@
-128, 0, 89, 112, 91, -34 };
private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes(
"hint", 219);
+ private static final long ONE_WEEK_IN_MS = 7 * 24 * 3600 * 1000;
+ private static final long ONE_DAY_IN_MS = 24 * 3600 * 1000;
+ private static final int[] NETWORK_EVENT_NUD_FAILURES = new int[] {
+ NETWORK_EVENT_NUD_FAILURE_ROAM,
+ NETWORK_EVENT_NUD_FAILURE_CONFIRM,
+ NETWORK_EVENT_NUD_FAILURE_ORGANIC,
+ NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED
+ };
@Mock
Context mMockContext;
@@ -333,4 +346,31 @@
mStore.factoryReset();
verify(mMockService, times(1)).factoryReset();
}
+
+ @Test
+ public void testNetworkEvents() throws Exception {
+ startIpMemoryStore(true /* supplyService */);
+ final String cluster = "cluster";
+
+ final long now = System.currentTimeMillis();
+ final long expiry = now + ONE_WEEK_IN_MS;
+ mStore.storeNetworkEvent(cluster, now, expiry, NETWORK_EVENT_NUD_FAILURE_ROAM,
+ status -> assertTrue("Store not successful : " + status.resultCode,
+ status.isSuccess()));
+ verify(mMockService, times(1)).storeNetworkEvent(eq(cluster),
+ eq(now), eq(expiry), eq(NETWORK_EVENT_NUD_FAILURE_ROAM), any());
+
+ final long[] sinceTimes = new long[2];
+ sinceTimes[0] = now - ONE_WEEK_IN_MS;
+ sinceTimes[1] = now - ONE_DAY_IN_MS;
+ mStore.retrieveNetworkEventCount(cluster, sinceTimes, NETWORK_EVENT_NUD_FAILURES,
+ (status, counts) -> {
+ assertTrue("Retrieve network event counts not successful : "
+ + status.resultCode, status.isSuccess());
+ assertEquals(new int[0], counts);
+ });
+
+ verify(mMockService, times(1)).retrieveNetworkEventCount(eq(cluster), eq(sinceTimes),
+ eq(NETWORK_EVENT_NUD_FAILURES), any());
+ }
}
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
new file mode 100644
index 0000000..c61541e
--- /dev/null
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -0,0 +1,46 @@
+/*
+* Copyright (C) 2024 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 android.net
+
+import android.net.TrafficStats.getValueForTypeFromFirstEntry
+import android.net.TrafficStats.TYPE_RX_BYTES
+import android.net.TrafficStats.UNSUPPORTED
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+private const val TEST_IFACE1 = "test_iface1"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class TrafficStatsTest {
+
+ @Test
+ fun testGetValueForTypeFromFirstEntry() {
+ var stats: NetworkStats = NetworkStats(0, 0)
+ // empty stats
+ assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), UNSUPPORTED.toLong())
+ // invalid type
+ stats.insertEntry(TEST_IFACE1, 1, 2, 3, 4)
+ assertEquals(getValueForTypeFromFirstEntry(stats, 1000), UNSUPPORTED.toLong())
+ // valid type
+ assertEquals(getValueForTypeFromFirstEntry(stats, TYPE_RX_BYTES), 1)
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index b5c0132..d801fba 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,6 +19,8 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
@@ -32,10 +34,12 @@
import android.net.Network;
import android.os.Handler;
import android.os.HandlerThread;
+import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.Pair;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -55,7 +59,9 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsDiscoveryManager}. */
@DevSdkIgnoreRunner.MonitorThreadLeak
@@ -96,6 +102,7 @@
@Mock MdnsServiceBrowserListener mockListenerOne;
@Mock MdnsServiceBrowserListener mockListenerTwo;
@Mock SharedLog sharedLog;
+ @Mock MdnsServiceCache mockServiceCache;
private MdnsDiscoveryManager discoveryManager;
private HandlerThread thread;
private Handler handler;
@@ -139,7 +146,9 @@
return null;
}
};
+ discoveryManager = makeDiscoveryManager(MdnsFeatureFlags.newBuilder().build());
doReturn(mockExecutorService).when(mockServiceTypeClientType1NullNetwork).getExecutor();
+ doReturn(mockExecutorService).when(mockServiceTypeClientType1Network1).getExecutor();
}
@After
@@ -150,6 +159,40 @@
}
}
+ private MdnsDiscoveryManager makeDiscoveryManager(@NonNull MdnsFeatureFlags featureFlags) {
+ return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog, featureFlags) {
+ @Override
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
+ @NonNull SocketKey socketKey) {
+ createdServiceTypeClientCount++;
+ final Pair<String, SocketKey> perSocketServiceType =
+ Pair.create(serviceType, socketKey);
+ if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
+ return mockServiceTypeClientType1NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_1_NETWORK_1)) {
+ return mockServiceTypeClientType1Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NULL_NETWORK)) {
+ return mockServiceTypeClientType2NullNetwork;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_1)) {
+ return mockServiceTypeClientType2Network1;
+ } else if (perSocketServiceType.equals(
+ PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
+ return mockServiceTypeClientType2Network2;
+ }
+ fail("Unexpected perSocketServiceType: " + perSocketServiceType);
+ return null;
+ }
+
+ @Override
+ MdnsServiceCache getServiceCache() {
+ return mockServiceCache;
+ }
+ };
+ }
+
private void runOnHandler(Runnable r) {
handler.post(r);
HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
@@ -390,6 +433,99 @@
verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
}
+ @Test
+ public void testDiscoveryExecutor() throws Exception {
+ final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
+ final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
+ try {
+ // Verify the checkAndRunOnHandlerThread method
+ final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
+ executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
+ assertTrue(future1.isDone());
+ assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the execute method
+ final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
+ executor.execute(()-> future2.complete(true));
+ testableLooper.processAllMessages();
+ assertTrue(future2.isDone());
+ assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Verify the executeDelayed method
+ final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
+ // Schedule a task with 999 ms delay
+ executor.executeDelayed(()-> future3.complete(true), 999L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed but do not exceed the target time (999 ms)
+ // The function should not be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertFalse(future3.isDone());
+
+ // 500 ms have elapsed again and have exceeded the target time (999 ms).
+ // The function should be executed.
+ testableLooper.moveTimeForward(500L);
+ testableLooper.processAllMessages();
+ assertTrue(future3.isDone());
+ assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
+ } finally {
+ testableLooper.destroy();
+ }
+ }
+
+ @Test
+ public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ doReturn(true).when(mockServiceTypeClientType1Network1)
+ .stopSendAndReceive(mockListenerOne);
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceTypeClientType1Network1).stopSendAndReceive(mockListenerOne);
+ verify(socketClient).stopDiscovery();
+ verify(mockServiceCache).removeServices(cacheKey);
+ }
+
+ @Test
+ public void testRemoveServicesAfterSocketDestroyed() throws IOException {
+ final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
+ .setIsCachedServicesRemovalEnabled(true)
+ .setCachedServicesRetentionTime(0L)
+ .build();
+ discoveryManager = makeDiscoveryManager(mdnsFeatureFlags);
+
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final MdnsServiceCache.CacheKey cacheKey =
+ new MdnsServiceCache.CacheKey(SERVICE_TYPE_1, SOCKET_KEY_NETWORK_1);
+ doReturn(cacheKey).when(mockServiceTypeClientType1Network1).getCacheKey();
+ runOnHandler(() -> callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
+ verify(executorProvider).shutdownExecutorService(mockExecutorService);
+ verify(mockServiceCache).removeServices(cacheKey);
+ }
+
private MdnsPacket createMdnsPacket(String serviceType) {
final String[] type = TextUtils.split(serviceType, "\\.");
final ArrayList<String> name = new ArrayList<>(type.length + 1);
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 3d2f389..ef4c44d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,6 +56,7 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.TrafficStats.getValueForTypeFromFirstEntry;
import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
@@ -72,13 +73,13 @@
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
-import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
-import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
@@ -620,8 +621,9 @@
}
@Override
- public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
- return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+ public boolean alwaysUseTrafficStatsServiceRateLimitCache(Context ctx) {
+ return mFeatureFlags.getOrDefault(
+ TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
}
@Override
@@ -636,8 +638,8 @@
}
@Override
- public int getTrafficStatsRateLimitCacheMaxEntries() {
- return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+ public int getTrafficStatsServiceRateLimitCacheMaxEntries() {
+ return DEFAULT_TRAFFIC_STATS_SERVICE_CACHE_MAX_ENTRIES;
}
@Override
@@ -2451,28 +2453,28 @@
assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@Test
public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
doTestTrafficStatsRateLimitCache(true /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
@Test
public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
doTestTrafficStatsRateLimitCache(false /* expectCached */);
}
- @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @FeatureFlag(name = TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG)
@Test
public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
@@ -2514,11 +2516,13 @@
private void assertTrafficStatsValues(String iface, int uid, long rxBytes, long rxPackets,
long txBytes, long txPackets) {
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getTotalStats(type));
+ (type) -> getValueForTypeFromFirstEntry(mService.getTypelessTotalStats(), type));
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getIfaceStats(iface, type));
+ (type) -> getValueForTypeFromFirstEntry(
+ mService.getTypelessIfaceStats(iface), type)
+ );
assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
- (type) -> mService.getUidStats(uid, type));
+ (type) -> getValueForTypeFromFirstEntry(mService.getTypelessUidStats(uid), type));
}
private void assertTrafficStatsValuesThat(long rxBytes, long rxPackets, long txBytes,
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index edf000a..838c0d9 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -23,8 +23,8 @@
// See https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
// for details of versioned rc files.
prebuilt_etc {
- name: "ot-daemon.init.34rc",
+ name: "ot-daemon.34rc",
src: "ot-daemon.34rc",
- filename: "init.34rc",
+ filename: "ot-daemon.34rc",
installable: false,
}
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index 117b4f9..a786639 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -32,7 +32,7 @@
"guava",
],
libs: [
- "framework-connectivity-t",
+ "framework-connectivity-t.stubs.module_lib",
],
required: [
"privapp-permissions-com.android.threadnetwork.demoapp",
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
index 257999b..f90a23b 100644
--- a/thread/docs/build-an-android-border-router.md
+++ b/thread/docs/build-an-android-border-router.md
@@ -169,7 +169,7 @@
user thread_network
```
-For real RCP devices, it supports both SPI and UART interace and you can
+For real RCP devices, it supports both SPI and UART interfaces and you can
specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
`spinel+socket://` respectively.
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index ecaefd0..cb4e8de 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -706,9 +706,9 @@
/**
* Sets max power of each channel.
*
- * <p>This method sets the max power for the given channel. The platform sets the actual
- * output power to be less than or equal to the {@code channelMaxPowers} and as close as
- * possible to the {@code channelMaxPowers}.
+ * <p>This method sets the max power for the given channel. The platform sets the actual output
+ * power to be less than or equal to the {@code channelMaxPowers} and as close as possible to
+ * the {@code channelMaxPowers}.
*
* <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
* chip firmware.
@@ -726,13 +726,13 @@
* and corresponding max power. Valid channel values should be between {@link
* ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
* ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. For
- * example, 1000 means 0.01W and 2000 means 0.1W. If the power value of
- * {@code channelMaxPowers} is lower than the minimum output power supported by the
- * platform, the output power will be set to the minimum output power supported by the
- * platform. If the power value of {@code channelMaxPowers} is higher than the maximum
- * output power supported by the platform, the output power will be set to the maximum
- * output power supported by the platform. If the power value of {@code channelMaxPowers}
- * is set to {@link #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
+ * example, 1000 means 0.01W and 2000 means 0.1W. If the power value of {@code
+ * channelMaxPowers} is lower than the minimum output power supported by the platform, the
+ * output power will be set to the minimum output power supported by the platform. If the
+ * power value of {@code channelMaxPowers} is higher than the maximum output power supported
+ * by the platform, the output power will be set to the maximum output power supported by
+ * the platform. If the power value of {@code channelMaxPowers} is set to {@link
+ * #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
* @param executor the executor to execute {@code receiver}.
* @param receiver the receiver to receive the result of this operation.
* @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index a82a499..1f4e601 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -37,7 +37,7 @@
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"framework-location.stubs.module_lib",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
"service-connectivity-pre-jarjar",
"ServiceConnectivityResources",
],
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 13817cf..362ca7e 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -447,7 +447,7 @@
}
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
- requestThreadNetwork();
+ registerThreadNetworkCallback();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
maybeInitializeOtDaemon();
@@ -768,7 +768,7 @@
}
}
- private void requestThreadNetwork() {
+ private void registerThreadNetworkCallback() {
mConnectivityManager.registerNetworkCallback(
new NetworkRequest.Builder()
// clearCapabilities() is needed to remove forbidden capabilities and UID
@@ -1248,7 +1248,7 @@
.setInfraLinkState(
mInfraLinkState,
infraIcmp6Socket,
- new setInfraLinkStateStatusReceiver());
+ new LoggingOtStatusReceiver("setInfraLinkState"));
} catch (RemoteException | ThreadNetworkException e) {
LOG.e("Failed to configure border router " + mOtDaemonConfig, e);
}
@@ -1397,32 +1397,21 @@
}
}
- private static final class setOtDaemonConfigurationStatusReceiver
- extends IOtStatusReceiver.Stub {
- public setOtDaemonConfigurationStatusReceiver() {}
+ private static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub {
+ private final String mAction;
+
+ LoggingOtStatusReceiver(String action) {
+ mAction = action;
+ }
@Override
public void onSuccess() {
- LOG.i("Configured border router successfully");
+ LOG.i("The action " + mAction + " succeeded");
}
@Override
public void onError(int i, String s) {
- LOG.w(String.format("Failed to set configurations: %d %s", i, s));
- }
- }
-
- private static final class setInfraLinkStateStatusReceiver extends IOtStatusReceiver.Stub {
- public setInfraLinkStateStatusReceiver() {}
-
- @Override
- public void onSuccess() {
- LOG.i("Set the infra link state successfully");
- }
-
- @Override
- public void onError(int i, String s) {
- LOG.w(String.format("Failed to set the infra link state: %d %s", i, s));
+ LOG.w("The action " + mAction + " failed: " + i + " " + s);
}
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 6572755..2630d21 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -49,8 +49,8 @@
"truth",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs",
+ "android.test.runner.stubs",
"framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 6eda1e9..34aabe2 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -22,6 +22,7 @@
<option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<!--
Only run tests if the device under test is SDK version 33 (Android 13) or above.
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 71693af..8f082a4 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -37,9 +37,9 @@
"ot-daemon-aidl-java",
],
libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
+ "android.test.runner.stubs",
+ "android.test.base.stubs",
+ "android.test.mock.stubs",
],
}
@@ -58,6 +58,7 @@
],
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
],
compile_multilib: "both",
}
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 9e8dc3a..103282a 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -34,7 +34,6 @@
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@@ -49,11 +48,9 @@
import android.content.Context;
import android.net.IpPrefix;
import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.MacAddress;
-import android.net.RouteInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
+import android.net.thread.utils.IntegrationTestUtils;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
@@ -634,32 +631,16 @@
}
private void setUpInfraNetwork() throws Exception {
- LinkProperties lp = new LinkProperties();
- // NAT64 feature requires the infra network to have an IPv4 default route.
- lp.addRoute(
- new RouteInfo(
- new IpPrefix("0.0.0.0/0") /* destination */,
- null /* gateway */,
- null,
- RouteInfo.RTN_UNICAST,
- 1500 /* mtu */));
- mInfraNetworkTracker =
- runAsShell(
- MANAGE_TEST_NETWORKS,
- () -> initTestNetwork(mContext, lp, 5000 /* timeoutMs */));
- String infraNetworkName = mInfraNetworkTracker.getTestIface().getInterfaceName();
- mController.setTestNetworkAsUpstreamAndWait(infraNetworkName);
+ mInfraNetworkTracker = IntegrationTestUtils.setUpInfraNetwork(mContext, mController);
}
private void tearDownInfraNetwork() {
- runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ IntegrationTestUtils.tearDownInfraNetwork(mInfraNetworkTracker);
}
- private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
+ private void startInfraDeviceAndWaitForOnLinkAddr() {
mInfraDevice =
- new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
- mInfraDevice.runSlaac(Duration.ofSeconds(60));
- assertNotNull(mInfraDevice.ipv6Addr);
+ IntegrationTestUtils.startInfraDeviceAndWaitForOnLinkAddr(mInfraNetworkReader);
}
private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
deleted file mode 100644
index 82e9332..0000000
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ /dev/null
@@ -1,563 +0,0 @@
-/*
- * Copyright (C) 2023 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 android.net.thread.utils;
-
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
-import static android.system.OsConstants.IPPROTO_ICMP;
-import static android.system.OsConstants.IPPROTO_ICMPV6;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
-import static org.junit.Assert.assertNotNull;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.net.ConnectivityManager;
-import android.net.InetAddresses;
-import android.net.LinkAddress;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.net.TestNetworkInterface;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.net.thread.ActiveOperationalDataset;
-import android.net.thread.ThreadNetworkController;
-import android.os.Build;
-import android.os.Handler;
-import android.os.SystemClock;
-
-import androidx.annotation.NonNull;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.net.module.util.Struct;
-import com.android.net.module.util.structs.Icmpv4Header;
-import com.android.net.module.util.structs.Icmpv6Header;
-import com.android.net.module.util.structs.Ipv4Header;
-import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.PrefixInformationOption;
-import com.android.net.module.util.structs.RaHeader;
-import com.android.testutils.HandlerUtils;
-import com.android.testutils.TapPacketReader;
-
-import com.google.common.util.concurrent.SettableFuture;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.nio.ByteBuffer;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-
-/** Static utility methods relating to Thread integration tests. */
-public final class IntegrationTestUtils {
- // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
- // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
- // seconds to be safe
- public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
- public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
- public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
- public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
- public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
-
- // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
- private static final byte[] DEFAULT_DATASET_TLVS =
- base16().decode(
- "0E080000000000010000000300001335060004001FFFE002"
- + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
- + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
- + "642D643961300102D9A00410A245479C836D551B9CA557F7"
- + "B9D351B40C0402A0FFF8");
- public static final ActiveOperationalDataset DEFAULT_DATASET =
- ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
-
- private IntegrationTestUtils() {}
-
- /**
- * Waits for the given {@link Supplier} to be true until given timeout.
- *
- * @param condition the condition to check
- * @param timeout the time to wait for the condition before throwing
- * @throws TimeoutException if the condition is still not met when the timeout expires
- */
- public static void waitFor(Supplier<Boolean> condition, Duration timeout)
- throws TimeoutException {
- final long intervalMills = 500;
- final long timeoutMills = timeout.toMillis();
-
- for (long i = 0; i < timeoutMills; i += intervalMills) {
- if (condition.get()) {
- return;
- }
- SystemClock.sleep(intervalMills);
- }
- if (condition.get()) {
- return;
- }
- throw new TimeoutException("The condition failed to become true in " + timeout);
- }
-
- /**
- * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
- *
- * @param testNetworkInterface the TUN interface of the test network
- * @param handler the handler to process the packets
- * @return the {@link TapPacketReader}
- */
- public static TapPacketReader newPacketReader(
- TestNetworkInterface testNetworkInterface, Handler handler) {
- FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
- final TapPacketReader reader =
- new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
- handler.post(() -> reader.start());
- HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
- return reader;
- }
-
- /**
- * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
- *
- * @param controller the {@link ThreadNetworkController}
- * @param deviceRoles the desired device roles. See also {@link
- * ThreadNetworkController.DeviceRole}
- * @param timeout the time to wait for the expected state before throwing
- * @return the {@link ThreadNetworkController.DeviceRole} after waiting
- * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
- * expires
- */
- public static int waitForStateAnyOf(
- ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
- throws TimeoutException {
- SettableFuture<Integer> future = SettableFuture.create();
- ThreadNetworkController.StateCallback callback =
- newRole -> {
- if (deviceRoles.contains(newRole)) {
- future.set(newRole);
- }
- };
- controller.registerStateCallback(directExecutor(), callback);
- try {
- return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
- } catch (InterruptedException | ExecutionException e) {
- throw new TimeoutException(
- String.format(
- "The device didn't become an expected role in %s: %s",
- timeout, e.getMessage()));
- } finally {
- controller.unregisterStateCallback(callback);
- }
- }
-
- /**
- * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
- *
- * @param packetReader a TUN packet reader
- * @param filter the filter to be applied on the packet
- * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
- * than 3000ms to read the next packet, the method will return null
- */
- public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
- byte[] packet;
- while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
- return packet;
- }
- return null;
- }
-
- /** Returns {@code true} if {@code packet} is an ICMPv4 packet of given {@code type}. */
- public static boolean isExpectedIcmpv4Packet(byte[] packet, int type) {
- ByteBuffer buf = makeByteBuffer(packet);
- Ipv4Header header = extractIpv4Header(buf);
- if (header == null) {
- return false;
- }
- if (header.protocol != (byte) IPPROTO_ICMP) {
- return false;
- }
- try {
- return Struct.parse(Icmpv4Header.class, buf).type == (short) type;
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return false;
- }
-
- /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
- public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
- ByteBuffer buf = makeByteBuffer(packet);
- Ipv6Header header = extractIpv6Header(buf);
- if (header == null) {
- return false;
- }
- if (header.nextHeader != (byte) IPPROTO_ICMPV6) {
- return false;
- }
- try {
- return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return false;
- }
-
- public static boolean isFrom(byte[] packet, InetAddress src) {
- if (src instanceof Inet4Address) {
- return isFromIpv4Source(packet, (Inet4Address) src);
- } else if (src instanceof Inet6Address) {
- return isFromIpv6Source(packet, (Inet6Address) src);
- }
- return false;
- }
-
- public static boolean isTo(byte[] packet, InetAddress dest) {
- if (dest instanceof Inet4Address) {
- return isToIpv4Destination(packet, (Inet4Address) dest);
- } else if (dest instanceof Inet6Address) {
- return isToIpv6Destination(packet, (Inet6Address) dest);
- }
- return false;
- }
-
- private static boolean isFromIpv4Source(byte[] packet, Inet4Address src) {
- Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
- return header != null && header.srcIp.equals(src);
- }
-
- private static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
- Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
- return header != null && header.srcIp.equals(src);
- }
-
- private static boolean isToIpv4Destination(byte[] packet, Inet4Address dest) {
- Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
- return header != null && header.dstIp.equals(dest);
- }
-
- private static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
- Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
- return header != null && header.dstIp.equals(dest);
- }
-
- private static ByteBuffer makeByteBuffer(byte[] packet) {
- return packet == null ? null : ByteBuffer.wrap(packet);
- }
-
- private static Ipv4Header extractIpv4Header(ByteBuffer buf) {
- try {
- return Struct.parse(Ipv4Header.class, buf);
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return null;
- }
-
- private static Ipv6Header extractIpv6Header(ByteBuffer buf) {
- try {
- return Struct.parse(Ipv6Header.class, buf);
- } catch (IllegalArgumentException ignored) {
- // It's fine that the passed in packet is malformed because it's could be sent
- // by anybody.
- }
- return null;
- }
-
- /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
- public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
- final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
-
- if (raMsg == null) {
- return pioList;
- }
-
- final ByteBuffer buf = ByteBuffer.wrap(raMsg);
- final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
- if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
- return pioList;
- }
-
- final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
- if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
- return pioList;
- }
-
- Struct.parse(RaHeader.class, buf);
- while (buf.position() < raMsg.length) {
- final int currentPos = buf.position();
- final int type = Byte.toUnsignedInt(buf.get());
- final int length = Byte.toUnsignedInt(buf.get());
- if (type == ICMPV6_ND_OPTION_PIO) {
- final ByteBuffer pioBuf =
- ByteBuffer.wrap(
- buf.array(),
- currentPos,
- Struct.getSize(PrefixInformationOption.class));
- final PrefixInformationOption pio =
- Struct.parse(PrefixInformationOption.class, pioBuf);
- pioList.add(pio);
-
- // Move ByteBuffer position to the next option.
- buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
- } else {
- // The length is in units of 8 octets.
- buf.position(currentPos + (length * 8));
- }
- }
- return pioList;
- }
-
- /**
- * Sends a UDP message to a destination.
- *
- * @param dstAddress the IP address of the destination
- * @param dstPort the port of the destination
- * @param message the message in UDP payload
- * @throws IOException if failed to send the message
- */
- public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
- throws IOException {
- SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
-
- try (DatagramSocket socket = new DatagramSocket()) {
- socket.connect(dstSockAddr);
-
- byte[] msgBytes = message.getBytes();
- DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
-
- socket.send(packet);
- }
- }
-
- public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
- final String cmd = "ip -6 maddr show dev " + interfaceName;
- final String output = runShellCommandOrThrow(cmd);
- final String addressStr = address.getHostAddress();
- for (final String line : output.split("\\n")) {
- if (line.contains(addressStr)) {
- return true;
- }
- }
- return false;
- }
-
- public static List<LinkAddress> getIpv6LinkAddresses(String interfaceName) {
- List<LinkAddress> addresses = new ArrayList<>();
- final String cmd = " ip -6 addr show dev " + interfaceName;
- final String output = runShellCommandOrThrow(cmd);
-
- for (final String line : output.split("\\n")) {
- if (line.contains("inet6")) {
- addresses.add(parseAddressLine(line));
- }
- }
-
- return addresses;
- }
-
- /** Return the first discovered service of {@code serviceType}. */
- public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
- throws Exception {
- CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- try {
- serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.stopServiceDiscovery(listener);
- }
-
- return serviceInfoFuture.get();
- }
-
- /**
- * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
- */
- public static NsdManager.DiscoveryListener discoverForServiceLost(
- NsdManager nsdManager,
- String serviceType,
- CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
- NsdManager.DiscoveryListener listener =
- new DefaultDiscoveryListener() {
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {
- serviceInfoFuture.complete(serviceInfo);
- }
- };
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
- return listener;
- }
-
- /** Resolves the service. */
- public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
- throws Exception {
- return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
- }
-
- /** Returns the first resolved service that satisfies the {@code predicate}. */
- public static NsdServiceInfo resolveServiceUntil(
- NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
- throws Exception {
- CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
- NsdManager.ServiceInfoCallback callback =
- new DefaultServiceInfoCallback() {
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
- if (predicate.test(serviceInfo)) {
- resolvedServiceInfoFuture.complete(serviceInfo);
- }
- }
- };
- nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
- try {
- return resolvedServiceInfoFuture.get(
- SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- nsdManager.unregisterServiceInfoCallback(callback);
- }
- }
-
- public static String getPrefixesFromNetData(String netData) {
- int startIdx = netData.indexOf("Prefixes:");
- int endIdx = netData.indexOf("Routes:");
- return netData.substring(startIdx, endIdx);
- }
-
- public static Network getThreadNetwork(Duration timeout) throws Exception {
- CompletableFuture<Network> networkFuture = new CompletableFuture<>();
- ConnectivityManager cm =
- ApplicationProvider.getApplicationContext()
- .getSystemService(ConnectivityManager.class);
- NetworkRequest.Builder networkRequestBuilder =
- new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD);
- // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
- // a Thread network.
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- networkRequestBuilder.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
- }
- NetworkRequest networkRequest = networkRequestBuilder.build();
- ConnectivityManager.NetworkCallback networkCallback =
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(Network network) {
- networkFuture.complete(network);
- }
- };
- cm.registerNetworkCallback(networkRequest, networkCallback);
- return networkFuture.get(timeout.toSeconds(), SECONDS);
- }
-
- /**
- * Let the FTD join the specified Thread network and wait for border routing to be available.
- *
- * @return the OMR address
- */
- public static Inet6Address joinNetworkAndWaitForOmr(
- FullThreadDevice ftd, ActiveOperationalDataset dataset) throws Exception {
- ftd.factoryReset();
- ftd.joinNetwork(dataset);
- ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
- waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
- Inet6Address ftdOmr = ftd.getOmrAddress();
- assertNotNull(ftdOmr);
- return ftdOmr;
- }
-
- private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
- @Override
- public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
-
- @Override
- public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
-
- @Override
- public void onDiscoveryStarted(String serviceType) {}
-
- @Override
- public void onDiscoveryStopped(String serviceType) {}
-
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {}
-
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {}
- }
-
- private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
- @Override
- public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
-
- @Override
- public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
-
- @Override
- public void onServiceLost() {}
-
- @Override
- public void onServiceInfoCallbackUnregistered() {}
- }
-
- /**
- * Parses a line of output from "ip -6 addr show" into a {@link LinkAddress}.
- *
- * <p>Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
- */
- private static LinkAddress parseAddressLine(String line) {
- String[] parts = line.trim().split("\\s+");
- String addressString = parts[1];
- String[] pieces = addressString.split("/", 2);
- int prefixLength = Integer.parseInt(pieces[1]);
- final InetAddress address = InetAddresses.parseNumericAddress(pieces[0]);
- long deprecationTimeMillis =
- line.contains("deprecated")
- ? SystemClock.elapsedRealtime()
- : LinkAddress.LIFETIME_PERMANENT;
-
- return new LinkAddress(
- address,
- prefixLength,
- 0 /* flags */,
- 0 /* scope */,
- deprecationTimeMillis,
- LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
- }
-}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
new file mode 100644
index 0000000..fa9855e
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2024 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 android.net.thread.utils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.RouteInfo
+import android.net.TestNetworkInterface
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import android.net.thread.ActiveOperationalDataset
+import android.net.thread.ThreadNetworkController
+import android.os.Build
+import android.os.Handler
+import android.os.SystemClock
+import android.system.OsConstants
+import androidx.test.core.app.ApplicationProvider
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.net.module.util.NetworkStackConstants
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv4Header
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.PrefixInformationOption
+import com.android.net.module.util.structs.RaHeader
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestNetworkTracker
+import com.android.testutils.initTestNetwork
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import com.google.common.io.BaseEncoding
+import com.google.common.util.concurrent.MoreExecutors
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import com.google.common.util.concurrent.SettableFuture
+import java.io.IOException
+import java.lang.Byte.toUnsignedInt
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.SocketAddress
+import java.nio.ByteBuffer
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import java.util.function.Predicate
+import java.util.function.Supplier
+import org.junit.Assert
+
+/** Utilities for Thread integration tests. */
+object IntegrationTestUtils {
+ // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+ // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+ // seconds to be safe
+ @JvmField
+ val RESTART_JOIN_TIMEOUT: Duration = Duration.ofSeconds(40)
+
+ @JvmField
+ val JOIN_TIMEOUT: Duration = Duration.ofSeconds(30)
+
+ @JvmField
+ val LEAVE_TIMEOUT: Duration = Duration.ofSeconds(2)
+
+ @JvmField
+ val CALLBACK_TIMEOUT: Duration = Duration.ofSeconds(1)
+
+ @JvmField
+ val SERVICE_DISCOVERY_TIMEOUT: Duration = Duration.ofSeconds(20)
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private val DEFAULT_DATASET_TLVS: ByteArray = BaseEncoding.base16().decode(
+ ("0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8")
+ )
+
+ @JvmField
+ val DEFAULT_DATASET: ActiveOperationalDataset =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
+
+ /**
+ * Waits for the given [Supplier] to be true until given timeout.
+ *
+ * @param condition the condition to check
+ * @param timeout the time to wait for the condition before throwing
+ * @throws TimeoutException if the condition is still not met when the timeout expires
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitFor(condition: Supplier<Boolean>, timeout: Duration) {
+ val intervalMills: Long = 500
+ val timeoutMills = timeout.toMillis()
+
+ var i: Long = 0
+ while (i < timeoutMills) {
+ if (condition.get()) {
+ return
+ }
+ SystemClock.sleep(intervalMills)
+ i += intervalMills
+ }
+ if (condition.get()) {
+ return
+ }
+ throw TimeoutException("The condition failed to become true in $timeout")
+ }
+
+ /**
+ * Creates a [TapPacketReader] given the [TestNetworkInterface] and [Handler].
+ *
+ * @param testNetworkInterface the TUN interface of the test network
+ * @param handler the handler to process the packets
+ * @return the [TapPacketReader]
+ */
+ @JvmStatic
+ fun newPacketReader(
+ testNetworkInterface: TestNetworkInterface, handler: Handler
+ ): TapPacketReader {
+ val fd = testNetworkInterface.fileDescriptor.fileDescriptor
+ val reader = TapPacketReader(handler, fd, testNetworkInterface.mtu)
+ handler.post { reader.start() }
+ handler.waitForIdle(timeoutMs = 5000)
+ return reader
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given `deviceRoles`.
+ *
+ * @param controller the [ThreadNetworkController]
+ * @param deviceRoles the desired device roles. See also [ ]
+ * @param timeout the time to wait for the expected state before throwing
+ * @return the [ThreadNetworkController.DeviceRole] after waiting
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires
+ */
+ @JvmStatic
+ @Throws(TimeoutException::class)
+ fun waitForStateAnyOf(
+ controller: ThreadNetworkController, deviceRoles: List<Int>, timeout: Duration
+ ): Int {
+ val future = SettableFuture.create<Int>()
+ val callback = ThreadNetworkController.StateCallback { newRole: Int ->
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole)
+ }
+ }
+ controller.registerStateCallback(MoreExecutors.directExecutor(), callback)
+ try {
+ return future[timeout.toMillis(), TimeUnit.MILLISECONDS]
+ } catch (e: InterruptedException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } catch (e: ExecutionException) {
+ throw TimeoutException(
+ "The device didn't become an expected role in $timeout: $e.message"
+ )
+ } finally {
+ controller.unregisterStateCallback(callback)
+ }
+ }
+
+ /**
+ * Polls for a packet from a given [TapPacketReader] that satisfies the `filter`.
+ *
+ * @param packetReader a TUN packet reader
+ * @param filter the filter to be applied on the packet
+ * @return the first IPv6 packet that satisfies the `filter`. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null
+ */
+ @JvmStatic
+ fun pollForPacket(packetReader: TapPacketReader, filter: Predicate<ByteArray>): ByteArray? {
+ var packet: ByteArray?
+ while ((packetReader.poll(3000 /* timeoutMs */, filter).also { packet = it }) != null) {
+ return packet
+ }
+ return null
+ }
+
+ /** Returns `true` if `packet` is an ICMPv4 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv4Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv4Header(buf) ?: return false
+ if (header.protocol != OsConstants.IPPROTO_ICMP.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv4Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ /** Returns `true` if `packet` is an ICMPv6 packet of given `type`. */
+ @JvmStatic
+ fun isExpectedIcmpv6Packet(packet: ByteArray, type: Int): Boolean {
+ val buf = makeByteBuffer(packet)
+ val header = extractIpv6Header(buf) ?: return false
+ if (header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return false
+ }
+ try {
+ return Struct.parse(Icmpv6Header::class.java, buf).type == type.toShort()
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun isFrom(packet: ByteArray, src: InetAddress): Boolean {
+ when (src) {
+ is Inet4Address -> return isFromIpv4Source(packet, src)
+ is Inet6Address -> return isFromIpv6Source(packet, src)
+ else -> return false
+ }
+ }
+
+ @JvmStatic
+ fun isTo(packet: ByteArray, dest: InetAddress): Boolean {
+ when (dest) {
+ is Inet4Address -> return isToIpv4Destination(packet, dest)
+ is Inet6Address -> return isToIpv6Destination(packet, dest)
+ else -> return false
+ }
+ }
+
+ private fun isFromIpv4Source(packet: ByteArray, src: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isFromIpv6Source(packet: ByteArray, src: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.srcIp == src
+ }
+
+ private fun isToIpv4Destination(packet: ByteArray, dest: Inet4Address): Boolean {
+ val header = extractIpv4Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun isToIpv6Destination(packet: ByteArray, dest: Inet6Address): Boolean {
+ val header = extractIpv6Header(makeByteBuffer(packet))
+ return header?.dstIp == dest
+ }
+
+ private fun makeByteBuffer(packet: ByteArray): ByteBuffer {
+ return ByteBuffer.wrap(packet)
+ }
+
+ private fun extractIpv4Header(buf: ByteBuffer): Ipv4Header? {
+ try {
+ return Struct.parse(Ipv4Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ private fun extractIpv6Header(buf: ByteBuffer): Ipv6Header? {
+ try {
+ return Struct.parse(Ipv6Header::class.java, buf)
+ } catch (ignored: IllegalArgumentException) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return null
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ @JvmStatic
+ fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
+ val pioList = ArrayList<PrefixInformationOption>()
+
+ raMsg ?: return pioList
+
+ val buf = ByteBuffer.wrap(raMsg)
+ val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+ if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
+ return pioList
+ }
+
+ val icmpv6Header = Struct.parse(Icmpv6Header::class.java, buf)
+ if (icmpv6Header.type != NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT.toShort()) {
+ return pioList
+ }
+
+ Struct.parse(RaHeader::class.java, buf)
+ while (buf.position() < raMsg.size) {
+ val currentPos = buf.position()
+ val type = toUnsignedInt(buf.get())
+ val length = toUnsignedInt(buf.get())
+ if (type == NetworkStackConstants.ICMPV6_ND_OPTION_PIO) {
+ val pioBuf = ByteBuffer.wrap(
+ buf.array(), currentPos, Struct.getSize(PrefixInformationOption::class.java)
+ )
+ val pio = Struct.parse(PrefixInformationOption::class.java, pioBuf)
+ pioList.add(pio)
+
+ // Move ByteBuffer position to the next option.
+ buf.position(
+ currentPos + Struct.getSize(PrefixInformationOption::class.java)
+ )
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8))
+ }
+ }
+ return pioList
+ }
+
+ /**
+ * Sends a UDP message to a destination.
+ *
+ * @param dstAddress the IP address of the destination
+ * @param dstPort the port of the destination
+ * @param message the message in UDP payload
+ * @throws IOException if failed to send the message
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun sendUdpMessage(dstAddress: InetAddress, dstPort: Int, message: String) {
+ val dstSockAddr: SocketAddress = InetSocketAddress(dstAddress, dstPort)
+
+ DatagramSocket().use { socket ->
+ socket.connect(dstSockAddr)
+ val msgBytes = message.toByteArray()
+ val packet = DatagramPacket(msgBytes, msgBytes.size)
+ socket.send(packet)
+ }
+ }
+
+ @JvmStatic
+ fun isInMulticastGroup(interfaceName: String, address: Inet6Address): Boolean {
+ val cmd = "ip -6 maddr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+ val addressStr = address.hostAddress
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains(addressStr)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ @JvmStatic
+ fun getIpv6LinkAddresses(interfaceName: String): List<LinkAddress> {
+ val addresses: MutableList<LinkAddress> = ArrayList()
+ val cmd = " ip -6 addr show dev $interfaceName"
+ val output: String = runShellCommandOrThrow(cmd)
+
+ for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
+ if (line.contains("inet6")) {
+ addresses.add(parseAddressLine(line))
+ }
+ }
+
+ return addresses
+ }
+
+ /** Return the first discovered service of `serviceType`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun discoverService(nsdManager: NsdManager, serviceType: String): NsdServiceInfo {
+ val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ try {
+ serviceInfoFuture[SERVICE_DISCOVERY_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.stopServiceDiscovery(listener)
+ }
+
+ return serviceInfoFuture.get()
+ }
+
+ /**
+ * Returns the [NsdServiceInfo] when a service instance of `serviceType` gets lost.
+ */
+ @JvmStatic
+ fun discoverForServiceLost(
+ nsdManager: NsdManager,
+ serviceType: String?,
+ serviceInfoFuture: CompletableFuture<NsdServiceInfo?>
+ ): NsdManager.DiscoveryListener {
+ val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
+ override fun onServiceLost(serviceInfo: NsdServiceInfo): Unit {
+ serviceInfoFuture.complete(serviceInfo)
+ }
+ }
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
+ return listener
+ }
+
+ /** Resolves the service. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveService(nsdManager: NsdManager, serviceInfo: NsdServiceInfo): NsdServiceInfo {
+ return resolveServiceUntil(nsdManager, serviceInfo) { true }
+ }
+
+ /** Returns the first resolved service that satisfies the `predicate`. */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun resolveServiceUntil(
+ nsdManager: NsdManager, serviceInfo: NsdServiceInfo, predicate: Predicate<NsdServiceInfo>
+ ): NsdServiceInfo {
+ val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
+ val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ if (predicate.test(serviceInfo)) {
+ resolvedServiceInfoFuture.complete(serviceInfo)
+ }
+ }
+ }
+ nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback)
+ try {
+ return resolvedServiceInfoFuture[
+ SERVICE_DISCOVERY_TIMEOUT.toMillis(),
+ TimeUnit.MILLISECONDS]
+ } finally {
+ nsdManager.unregisterServiceInfoCallback(callback)
+ }
+ }
+
+ @JvmStatic
+ fun getPrefixesFromNetData(netData: String): String {
+ val startIdx = netData.indexOf("Prefixes:")
+ val endIdx = netData.indexOf("Routes:")
+ return netData.substring(startIdx, endIdx)
+ }
+
+ @JvmStatic
+ @Throws(Exception::class)
+ fun getThreadNetwork(timeout: Duration): Network {
+ val networkFuture = CompletableFuture<Network>()
+ val cm =
+ ApplicationProvider.getApplicationContext<Context>()
+ .getSystemService(ConnectivityManager::class.java)
+ val networkRequestBuilder =
+ NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
+ // a Thread network.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ }
+ val networkRequest = networkRequestBuilder.build()
+ val networkCallback: ConnectivityManager.NetworkCallback =
+ object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ networkFuture.complete(network)
+ }
+ }
+ cm.registerNetworkCallback(networkRequest, networkCallback)
+ return networkFuture[timeout.toSeconds(), TimeUnit.SECONDS]
+ }
+
+ /**
+ * Let the FTD join the specified Thread network and wait for border routing to be available.
+ *
+ * @return the OMR address
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun joinNetworkAndWaitForOmr(
+ ftd: FullThreadDevice, dataset: ActiveOperationalDataset
+ ): Inet6Address {
+ ftd.factoryReset()
+ ftd.joinNetwork(dataset)
+ ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
+ waitFor({ ftd.omrAddress != null }, Duration.ofSeconds(60))
+ Assert.assertNotNull(ftd.omrAddress)
+ return ftd.omrAddress
+ }
+
+ private open class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
+ override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
+ override fun onDiscoveryStarted(serviceType: String) {}
+ override fun onDiscoveryStopped(serviceType: String) {}
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(serviceInfo: NsdServiceInfo) {}
+ }
+
+ private open class DefaultServiceInfoCallback : NsdManager.ServiceInfoCallback {
+ override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {}
+ override fun onServiceLost(): Unit {}
+ override fun onServiceInfoCallbackUnregistered() {}
+ }
+
+ /**
+ * Parses a line of output from "ip -6 addr show" into a [LinkAddress].
+ *
+ * Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
+ */
+ private fun parseAddressLine(line: String): LinkAddress {
+ val parts = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }.toTypedArray()
+ val addressString = parts[1]
+ val pieces = addressString.split("/".toRegex(), limit = 2).toTypedArray()
+ val prefixLength = pieces[1].toInt()
+ val address = parseNumericAddress(pieces[0])
+ val deprecationTimeMillis =
+ if (line.contains("deprecated")) SystemClock.elapsedRealtime()
+ else LinkAddress.LIFETIME_PERMANENT
+
+ return LinkAddress(
+ address, prefixLength,
+ 0 /* flags */, 0 /* scope */,
+ deprecationTimeMillis, LinkAddress.LIFETIME_PERMANENT /* expirationTime */
+ )
+ }
+
+ @JvmStatic
+ @JvmOverloads
+ fun startInfraDeviceAndWaitForOnLinkAddr(
+ tapPacketReader: TapPacketReader,
+ macAddress: MacAddress = MacAddress.fromString("1:2:3:4:5:6")
+ ): InfraNetworkDevice {
+ val infraDevice = InfraNetworkDevice(macAddress, tapPacketReader)
+ infraDevice.runSlaac(Duration.ofSeconds(60))
+ requireNotNull(infraDevice.ipv6Addr)
+ return infraDevice
+ }
+
+ @JvmStatic
+ @Throws(java.lang.Exception::class)
+ fun setUpInfraNetwork(
+ context: Context, controller: ThreadNetworkControllerWrapper
+ ): TestNetworkTracker {
+ val lp = LinkProperties()
+
+ // TODO: use a fake DNS server
+ lp.setDnsServers(listOf(parseNumericAddress("8.8.8.8")))
+ // NAT64 feature requires the infra network to have an IPv4 default route.
+ lp.addRoute(
+ RouteInfo(
+ IpPrefix("0.0.0.0/0") /* destination */,
+ null /* gateway */,
+ null /* iface */,
+ RouteInfo.RTN_UNICAST, 1500 /* mtu */
+ )
+ )
+ val infraNetworkTracker: TestNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ supplier = { initTestNetwork(context, lp, setupTimeoutMs = 5000) })
+ val infraNetworkName: String = infraNetworkTracker.testIface.getInterfaceName()
+ controller.setTestNetworkAsUpstreamAndWait(infraNetworkName)
+
+ return infraNetworkTracker
+ }
+
+ @JvmStatic
+ fun tearDownInfraNetwork(testNetworkTracker: TestNetworkTracker) {
+ runAsShell(MANAGE_TEST_NETWORKS) { testNetworkTracker.teardown() }
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 9404d1b..c6a24ea 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -50,10 +50,10 @@
"service-thread-pre-jarjar",
],
libs: [
- "android.test.base",
- "android.test.runner",
+ "android.test.base.stubs.system",
+ "android.test.runner.stubs.system",
"ServiceConnectivityResources",
- "framework-wifi",
+ "framework-wifi.stubs.module_lib",
],
jni_libs: [
"libservice-thread-jni",