Merge "Fix Lint error" into main
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index a0b2434..7b52694 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -62,6 +62,7 @@
test_suites: [
"cts",
"general-tests",
- "mts-tethering"
+ "mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 414e50a..73c11ba 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -94,14 +94,17 @@
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiCurrentShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_library {
@@ -109,14 +112,17 @@
defaults: [
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiStableShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
@@ -189,20 +195,28 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
// Updatable tethering packaged for finalized API
android_app {
name: "Tethering",
- defaults: ["TetheringAppDefaults", "TetheringApiLevel"],
+ defaults: [
+ "TetheringAppDefaults",
+ "TetheringApiLevel",
+ ],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_app {
@@ -221,6 +235,7 @@
lint: {
strict_updatability_linting: true,
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -239,19 +254,24 @@
java_library_static {
name: "tetheringstatsprotos",
- proto: {type: "lite"},
+ proto: {
+ type: "lite",
+ },
srcs: [
"src/com/android/networkstack/tethering/metrics/stats.proto",
],
static_libs: ["tetheringprotos"],
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-tethering-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
- " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+ " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e8d0c9..bcea425 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -43,6 +43,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c065cd6..a8c8408 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,7 +33,6 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
-import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -316,6 +315,7 @@
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
+ private final boolean mIsSyncSM;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
@@ -325,7 +325,7 @@
@Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+ super(ifaceName, config.isSyncSM() ? null : handler.getLooper());
mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
@@ -338,6 +338,7 @@
mLinkProperties = new LinkProperties();
mUsingLegacyDhcp = config.useLegacyDhcpServer();
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
+ mIsSyncSM = config.isSyncSM();
mPrivateAddressCoordinator = addressCoordinator;
mDeps = deps;
mTetheringMetrics = tetheringMetrics;
@@ -515,7 +516,7 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- if (USE_SYNC_SM) {
+ if (mIsSyncSM) {
sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
@@ -1170,7 +1171,7 @@
// in previous versions of the mainline module.
// TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
// StateMachine.
- if (USE_SYNC_SM) {
+ if (mIsSyncSM) {
sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 5e9bbcb..50d6c4b 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,7 +18,6 @@
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
-import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
@@ -39,21 +38,12 @@
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.SocketUtils;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
-import com.android.net.module.util.FdEventsReader;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.LlaOption;
@@ -113,11 +103,6 @@
private static final int DAY_IN_SECONDS = 86_400;
- // Commands for IpServer to control RouterAdvertisementDaemon
- private static final int CMD_START = 1;
- private static final int CMD_STOP = 2;
- private static final int CMD_BUILD_NEW_RA = 3;
-
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -135,13 +120,9 @@
@GuardedBy("mLock")
private RaParams mRaParams;
- // To be accessed only from RaMessageHandler
- private RsPacketListener mRsPacketListener;
-
private volatile FileDescriptor mSocket;
private volatile MulticastTransmitter mMulticastTransmitter;
- private volatile RaMessageHandler mRaMessageHandler;
- private volatile HandlerThread mRaHandlerThread;
+ private volatile UnicastResponder mUnicastResponder;
/** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
public static class RaParams {
@@ -263,94 +244,6 @@
}
}
- private class RaMessageHandler extends Handler {
- RaMessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case CMD_START:
- mRsPacketListener = new RsPacketListener(this);
- mRsPacketListener.start();
- break;
- case CMD_STOP:
- if (mRsPacketListener != null) {
- mRsPacketListener.stop();
- mRsPacketListener = null;
- }
- break;
- case CMD_BUILD_NEW_RA:
- synchronized (mLock) {
- // raInfo.first is deprecatedParams and raInfo.second is newParams.
- final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
- if (raInfo.first != null) {
- mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
- mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
- }
-
- if (raInfo.second != null) {
- // Process information that is no longer deprecated.
- mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
- mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
- }
- mRaParams = raInfo.second;
- assembleRaLocked();
- }
-
- maybeNotifyMulticastTransmitter();
- break;
- default:
- Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
- break;
- }
- }
- }
-
- private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
- private static final class RecvBuffer {
- // The recycled buffer for receiving Router Solicitations from clients.
- // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
- // This is fine since currently only byte 0 is examined anyway.
- final byte[] mBytes = new byte[IPV6_MIN_MTU];
- final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
- }
-
- RsPacketListener(@NonNull Handler handler) {
- super(handler, new RecvBuffer());
- }
-
- @Override
- protected int recvBufSize(@NonNull RecvBuffer buffer) {
- return buffer.mBytes.length;
- }
-
- @Override
- protected FileDescriptor createFd() {
- return mSocket;
- }
-
- @Override
- protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
- throws Exception {
- return Os.recvfrom(
- fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
- }
-
- @Override
- protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
- // Do the least possible amount of validations.
- if (buffer.mSrcAddr == null
- || length <= 0
- || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
- return;
- }
-
- maybeSendRA(buffer.mSrcAddr);
- }
- }
-
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -359,43 +252,48 @@
/** Build new RA.*/
public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
- final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
- sendMessage(CMD_BUILD_NEW_RA, raInfo);
+ synchronized (mLock) {
+ if (deprecatedParams != null) {
+ mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+ mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+ }
+
+ if (newParams != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+ mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+ }
+
+ mRaParams = newParams;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
}
/** Start router advertisement daemon. */
public boolean start() {
if (!createSocket()) {
- Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
return false;
}
mMulticastTransmitter = new MulticastTransmitter();
mMulticastTransmitter.start();
- mRaHandlerThread = new HandlerThread(TAG);
- mRaHandlerThread.start();
- mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
+ mUnicastResponder = new UnicastResponder();
+ mUnicastResponder.start();
- return sendMessage(CMD_START);
+ return true;
}
/** Stop router advertisement daemon. */
public void stop() {
- if (!sendMessage(CMD_STOP)) {
- Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
- return;
- }
-
- mRaHandlerThread.quitSafely();
- mRaHandlerThread = null;
- mRaMessageHandler = null;
-
closeSocket();
// Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
// the thread's termination.
maybeNotifyMulticastTransmitter();
mMulticastTransmitter = null;
+ mUnicastResponder = null;
}
@GuardedBy("mLock")
@@ -605,7 +503,7 @@
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
- mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+ mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -667,17 +565,34 @@
}
}
- private boolean sendMessage(int cmd) {
- return sendMessage(cmd, null);
- }
+ private final class UnicastResponder extends Thread {
+ private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
- private boolean sendMessage(int cmd, @Nullable Object obj) {
- if (mRaMessageHandler == null) {
- return false;
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ // Blocking receive.
+ final int rval = Os.recvfrom(
+ mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+ // Do the least possible amount of validation.
+ if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ continue;
+ }
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "recvfrom error: " + e);
+ }
+ continue;
+ }
+
+ maybeSendRA(mSolicitor);
+ }
}
-
- return mRaMessageHandler.sendMessage(
- Message.obtain(mRaMessageHandler, cmd, obj));
}
// TODO: Consider moving this to run on a provided Looper as a Handler,
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 0678525..d09183a 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -132,15 +132,15 @@
public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
"tether_force_random_prefix_base_selection";
+
+ public static final String TETHER_ENABLE_SYNC_SM = "tether_enable_sync_sm";
+
/**
* Default value that used to periodic polls tether offload stats from tethering offload HAL
* to make the data warnings work.
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
- /** A flag for using synchronous or asynchronous state machine. */
- public static final boolean USE_SYNC_SM = false;
-
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
@@ -174,6 +174,7 @@
private final boolean mEnableWearTethering;
private final boolean mRandomPrefixBase;
+ private final boolean mEnableSyncSm;
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
@@ -292,6 +293,7 @@
mEnableWearTethering = shouldEnableWearTethering(ctx);
mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
+ mEnableSyncSm = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
configLog.log(toString());
}
@@ -385,6 +387,10 @@
return mRandomPrefixBase;
}
+ public boolean isSyncSM() {
+ return mEnableSyncSm;
+ }
+
/** Does the dumping.*/
public void dump(PrintWriter pw) {
pw.print("activeDataSubId: ");
@@ -438,6 +444,9 @@
pw.print("mRandomPrefixBase: ");
pw.println(mRandomPrefixBase);
+
+ pw.print("mEnableSyncSm: ");
+ pw.println(mEnableSyncSm);
}
/** Returns the string representation of this object.*/
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 377da91..c232697 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
+
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -46,7 +48,6 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -56,8 +57,6 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -141,11 +140,12 @@
protected static final ByteBuffer TX_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
- private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
- private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
- private final PackageManager mPackageManager = mContext.getPackageManager();
- private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
+ private static final Context sContext =
+ InstrumentationRegistry.getInstrumentation().getContext();
+ private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
// Late initialization in setUp()
private boolean mRunTests;
@@ -161,7 +161,7 @@
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
- return mContext;
+ return sContext;
}
@BeforeClass
@@ -170,19 +170,24 @@
// Tethering would cache the last upstreams so that the next enabled tethering avoids
// picking up the address that is in conflict with the upstreams. To protect subsequent
// tests, turn tethering on and off before running them.
- final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
- final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
- final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
try {
- if (!callback.isWifiTetheringSupported(ctx)) return;
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
- callback.expectNoTetheringActive();
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
- utils.startWifiTethering(callback);
- callback.getCurrentValidUpstream();
- utils.stopWifiTethering(callback);
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
} finally {
- utils.unregisterTetheringEventCallback(callback);
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
}
}
@@ -195,13 +200,13 @@
mRunTests = isEthernetTetheringSupported();
assumeTrue(mRunTests);
- mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester();
}
private boolean isEthernetTetheringSupported() throws Exception {
- if (mEm == null) return false;
+ if (sEm == null) return false;
- return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+ return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -212,7 +217,7 @@
}
}
- protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+ protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
throws Exception {
if (testInterface != null) {
testInterface.getFileDescriptor().close();
@@ -220,8 +225,8 @@
}
}
- protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
- throws Exception {
+ protected static void maybeUnregisterTetheringEventCallback(
+ final MyTetheringEventCallback callback) throws Exception {
if (callback != null) {
callback.awaitInterfaceUntethered();
callback.unregister();
@@ -230,7 +235,7 @@
protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.stopTethering(TETHERING_ETHERNET);
+ sTm.stopTethering(TETHERING_ETHERNET);
maybeUnregisterTetheringEventCallback(callback);
});
}
@@ -277,18 +282,18 @@
}
}
- protected boolean isInterfaceForTetheringAvailable() throws Exception {
+ protected static boolean isInterfaceForTetheringAvailable() throws Exception {
// Before T, all ethernet interfaces could be used for server mode. Instead of
// waiting timeout, just checking whether the system currently has any
// ethernet interface is more reliable.
if (!SdkLevel.isAtLeastT()) {
- return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+ return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
}
// If previous test case doesn't release tethering interface successfully, the other tests
// after that test may be skipped as unexcepted.
// TODO: figure out a better way to check default tethering interface existenion.
- final TetheredInterfaceRequester requester = new TetheredInterfaceRequester(mHandler, mEm);
+ final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
try {
// Use short timeout (200ms) for requesting an existing interface, if any, because
// it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
}
}
- protected void setIncludeTestInterfaces(boolean include) {
+ protected static void setIncludeTestInterfaces(boolean include) {
runAsShell(NETWORK_SETTINGS, () -> {
- mEm.setIncludeTestInterfaces(include);
+ sEm.setIncludeTestInterfaces(include);
});
}
- protected void setPreferTestNetworks(boolean prefer) {
+ protected static void setPreferTestNetworks(boolean prefer) {
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.setPreferTestNetworks(prefer);
+ sTm.setPreferTestNetworks(prefer);
});
}
@@ -344,7 +349,6 @@
protected static final class MyTetheringEventCallback implements TetheringEventCallback {
- private final TetheringManager mTm;
private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -355,7 +359,7 @@
private final TetheringInterface mIface;
private final Network mExpectedUpstream;
- private boolean mAcceptAnyUpstream = false;
+ private final boolean mAcceptAnyUpstream;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
@@ -368,19 +372,21 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(TetheringManager tm, String iface) {
- this(tm, iface, null);
+ MyTetheringEventCallback(String iface) {
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
- mTm = tm;
+ MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ Objects.requireNonNull(expectedUpstream);
mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
mExpectedUpstream = expectedUpstream;
+ mAcceptAnyUpstream = false;
}
public void unregister() {
- mTm.unregisterTetheringEventCallback(this);
+ sTm.unregisterTetheringEventCallback(this);
mUnregistered = true;
}
@Override
@@ -504,6 +510,11 @@
Log.d(TAG, "Got upstream changed: " + network);
mUpstream = network;
+ // The callback always updates the current tethering status when it's first registered.
+ // If the caller registers the callback before tethering starts, the null upstream
+ // would be updated. Filtering out the null case because it's not a valid upstream that
+ // we care about.
+ if (mUpstream == null) return;
if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
mUpstreamLatch.countDown();
}
@@ -525,18 +536,18 @@
}
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
// after etherent tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+ callback = new MyTetheringEventCallback(iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(mTm, iface);
+ callback = new MyTetheringEventCallback(iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
// Need to hold the shell permission until callback is registered. This helps to avoid
// the test become flaky.
callback.awaitCallbackRegistered();
@@ -556,7 +567,7 @@
};
Log.d(TAG, "Starting Ethernet tethering");
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+ sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -579,7 +590,7 @@
return callback;
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
return enableEthernetTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -605,17 +616,9 @@
}
protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
- private final Handler mHandler;
- private final EthernetManager mEm;
-
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
- TetheredInterfaceRequester(Handler handler, EthernetManager em) {
- mHandler = handler;
- mEm = em;
- }
-
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
assertNull("BUG: more than one tethered interface request", mRequest);
Log.d(TAG, "Requesting tethered interface");
mRequest = runAsShell(NETWORK_SETTINGS, () ->
- mEm.requestTetheredInterface(mHandler::post, this));
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
return mFuture;
}
@@ -652,9 +655,9 @@
}
}
- protected TestNetworkInterface createTestInterface() throws Exception {
+ protected static TestNetworkInterface createTestInterface() throws Exception {
TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
- mContext.getSystemService(TestNetworkManager.class));
+ sContext.getSystemService(TestNetworkManager.class));
TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
tnm.createTapInterface());
Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -669,7 +672,7 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,7 @@
private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
final TimeoutException fallbackException) throws Exception {
// Fall back original exception because no way to reselect if there is no WIFI feature.
- assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
// Try to toggle wifi network, if any, to reselect upstream network via default network
// switching. Because test network has higher priority than internet network, this can
@@ -867,7 +870,7 @@
// See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- mCtsNetUtils.toggleWifi();
+ sCtsNetUtils.toggleWifi();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -881,14 +884,14 @@
}
};
try {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(mHandler::post, callback);
assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new AssertionError("Did not receive upstream " + expectedUpstream
+ " callback after " + TIMEOUT_MS + "ms");
} finally {
- mTm.unregisterTetheringEventCallback(callback);
+ sTm.unregisterTetheringEventCallback(callback);
}
}
@@ -925,7 +928,7 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
- final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
// sure tethering already have ipv6 connectivity before testing.
if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index aa322dc..19c6e5a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -754,4 +754,26 @@
new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
}
+
+ private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
+ mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+ }
+
+ private void assertEnableSyncSMIs(boolean value) {
+ assertEquals(value, new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).isSyncSM());
+ }
+
+ @Test
+ public void testEnableSyncSMFlag() throws Exception {
+ // Test default disabled
+ setTetherEnableSyncSMFlagEnabled(null);
+ assertEnableSyncSMIs(false);
+
+ setTetherEnableSyncSMFlagEnabled(true);
+ assertEnableSyncSMIs(true);
+
+ setTetherEnableSyncSMFlagEnabled(false);
+ assertEnableSyncSMIs(false);
+ }
}
diff --git a/common/Android.bp b/common/Android.bp
index 1d73a46..f2f3929 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,12 +19,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-build = ["TrunkStable.bp"]
-
-// This is a placeholder comment to avoid merge conflicts
-// as the above target may not exist
-// depending on the branch
-
// The library requires the final artifact to contain net-utils-device-common-struct.
java_library {
name: "connectivity-net-module-utils-bpf",
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
deleted file mode 100644
index 59874c2..0000000
--- a/common/TrunkStable.bp
+++ /dev/null
@@ -1,16 +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.
-//
-
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index deda74e..c31dcf5 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -57,6 +57,10 @@
"app-compat-annotations",
"androidx.annotation_annotation",
],
+ static_libs: [
+ // Cannot go to framework-connectivity because mid_sdk checks require 31.
+ "modules-utils-binary-xml",
+ ],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
@@ -182,6 +186,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..60a88c0 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,6 +127,7 @@
public final class IpSecTransform implements java.lang.AutoCloseable {
method public void close();
+ method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
}
public static class IpSecTransform.Builder {
@@ -138,6 +139,29 @@
method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
}
+ @FlaggedApi("com.android.net.flags.ipsec_transform_state") public final class IpSecTransformState implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getByteCount();
+ method public long getPacketCount();
+ method @NonNull public byte[] getReplayBitmap();
+ method public long getRxHighestSequenceNumber();
+ method public long getTimestamp();
+ method public long getTxHighestSequenceNumber();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.flags.ipsec_transform_state") public static final class IpSecTransformState.Builder {
+ ctor public IpSecTransformState.Builder();
+ method @NonNull public android.net.IpSecTransformState build();
+ method @NonNull public android.net.IpSecTransformState.Builder setByteCount(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
+ method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
+ }
+
public class TrafficStats {
ctor public TrafficStats();
method public static void clearThreadStatsTag();
@@ -251,6 +275,7 @@
method public int getPort();
method public String getServiceName();
method public String getServiceType();
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
method public void removeAttribute(String);
method public void setAttribute(String, String);
method @Deprecated public void setHost(java.net.InetAddress);
@@ -259,6 +284,7 @@
method public void setPort(int);
method public void setServiceName(String);
method public void setServiceType(String);
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
}
diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
index 88ffd0e..f972ab9 100644
--- a/framework-t/src/android/net/IIpSecService.aidl
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -22,6 +22,7 @@
import android.net.IpSecUdpEncapResponse;
import android.net.IpSecSpiResponse;
import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
import android.net.IpSecTunnelInterfaceResponse;
import android.os.Bundle;
import android.os.IBinder;
@@ -74,6 +75,8 @@
void deleteTransform(int transformId);
+ IpSecTransformState getTransformState(int transformId);
+
void applyTransportModeTransform(
in ParcelFileDescriptor socket, int direction, int transformId);
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3afa6ef..3f74e1c 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,6 +65,13 @@
public class IpSecManager {
private static final String TAG = "IpSecManager";
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
+ }
+
/**
* Feature flag to declare the kernel support of updating IPsec SAs.
*
@@ -1084,6 +1091,12 @@
}
}
+ /** @hide */
+ public IpSecTransformState getTransformState(int transformId)
+ throws IllegalStateException, RemoteException {
+ return mService.getTransformState(transformId);
+ }
+
/**
* Construct an instance of IpSecManager within an application context.
*
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index c236b6c..246a2dd 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,8 +15,11 @@
*/
package android.net;
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
import static android.net.IpSecManager.INVALID_RESOURCE_ID;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -26,6 +29,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.Log;
@@ -38,6 +43,7 @@
import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* This class represents a transform, which roughly corresponds to an IPsec Security Association.
@@ -201,6 +207,43 @@
}
/**
+ * Retrieve the current state of this IpSecTransform.
+ *
+ * @param executor The {@link Executor} on which to call the supplied callback.
+ * @param callback Callback that's called after the transform state is ready or when an error
+ * occurs.
+ * @see IpSecTransformState
+ */
+ @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ public void getIpSecTransformState(
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ // TODO: Consider adding check to prevent DDoS attack.
+
+ try {
+ final IpSecTransformState ipSecTransformState =
+ getIpSecManager(mContext).getTransformState(mResourceId);
+ executor.execute(
+ () -> {
+ callback.onResult(ipSecTransformState);
+ });
+ } catch (IllegalStateException e) {
+ executor.execute(
+ () -> {
+ callback.onError(e);
+ });
+ } catch (RemoteException e) {
+ executor.execute(
+ () -> {
+ callback.onError(e.rethrowFromSystemServer());
+ });
+ }
+ }
+
+ /**
* A callback class to provide status information regarding a NAT-T keepalive session
*
* <p>Use this callback to receive status information regarding a NAT-T keepalive session
diff --git a/framework-t/src/android/net/IpSecTransformState.aidl b/framework-t/src/android/net/IpSecTransformState.aidl
new file mode 100644
index 0000000..69cce28
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @hide */
+parcelable IpSecTransformState;
\ No newline at end of file
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
new file mode 100644
index 0000000..b575dd5
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -0,0 +1,282 @@
+/*
+ * 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;
+
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+
+import java.util.Objects;
+
+/**
+ * This class represents a snapshot of the state of an IpSecTransform
+ *
+ * <p>This class provides the current state of an IpSecTransform, enabling link metric analysis by
+ * the caller. Use cases include understanding transform usage, such as packet and byte counts, as
+ * well as observing out-of-order delivery by checking the bitmap. Additionally, callers can query
+ * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
+ * numbers, callers can estimate IPsec data loss in the inbound direction.
+ */
+@FlaggedApi(IPSEC_TRANSFORM_STATE)
+public final class IpSecTransformState implements Parcelable {
+ private final long mTimeStamp;
+ private final long mTxHighestSequenceNumber;
+ private final long mRxHighestSequenceNumber;
+ private final long mPacketCount;
+ private final long mByteCount;
+ private final byte[] mReplayBitmap;
+
+ private IpSecTransformState(
+ long timestamp,
+ long txHighestSequenceNumber,
+ long rxHighestSequenceNumber,
+ long packetCount,
+ long byteCount,
+ byte[] replayBitmap) {
+ mTimeStamp = timestamp;
+ mTxHighestSequenceNumber = txHighestSequenceNumber;
+ mRxHighestSequenceNumber = rxHighestSequenceNumber;
+ mPacketCount = packetCount;
+ mByteCount = byteCount;
+
+ Objects.requireNonNull(replayBitmap, "replayBitmap is null");
+ mReplayBitmap = replayBitmap.clone();
+
+ validate();
+ }
+
+ private void validate() {
+ Objects.requireNonNull(mReplayBitmap, "mReplayBitmap is null");
+ }
+
+ /**
+ * Deserializes a IpSecTransformState from a PersistableBundle.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ public IpSecTransformState(@NonNull Parcel in) {
+ Objects.requireNonNull(in, "The input PersistableBundle is null");
+ mTimeStamp = in.readLong();
+ mTxHighestSequenceNumber = in.readLong();
+ mRxHighestSequenceNumber = in.readLong();
+ mPacketCount = in.readLong();
+ mByteCount = in.readLong();
+ mReplayBitmap = HexDump.hexStringToByteArray(in.readString());
+
+ validate();
+ }
+
+ // Parcelable methods
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeLong(mTimeStamp);
+ out.writeLong(mTxHighestSequenceNumber);
+ out.writeLong(mRxHighestSequenceNumber);
+ out.writeLong(mPacketCount);
+ out.writeLong(mByteCount);
+ out.writeString(HexDump.toHexString(mReplayBitmap));
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<IpSecTransformState> CREATOR =
+ new Parcelable.Creator<IpSecTransformState>() {
+ @NonNull
+ public IpSecTransformState createFromParcel(Parcel in) {
+ return new IpSecTransformState(in);
+ }
+
+ @NonNull
+ public IpSecTransformState[] newArray(int size) {
+ return new IpSecTransformState[size];
+ }
+ };
+
+ /**
+ * Retrieve the epoch timestamp (milliseconds) for when this state was created
+ *
+ * @see Builder#setTimestamp(long)
+ */
+ public long getTimestamp() {
+ return mTimeStamp;
+ }
+
+ /**
+ * Retrieve the highest sequence number sent so far
+ *
+ * @see Builder#setTxHighestSequenceNumber(long)
+ */
+ public long getTxHighestSequenceNumber() {
+ return mTxHighestSequenceNumber;
+ }
+
+ /**
+ * Retrieve the highest sequence number received so far
+ *
+ * @see Builder#setRxHighestSequenceNumber(long)
+ */
+ public long getRxHighestSequenceNumber() {
+ return mRxHighestSequenceNumber;
+ }
+
+ /**
+ * Retrieve the number of packets received AND sent so far
+ *
+ * @see Builder#setPacketCount(long)
+ */
+ public long getPacketCount() {
+ return mPacketCount;
+ }
+
+ /**
+ * Retrieve the number of bytes received AND sent so far
+ *
+ * @see Builder#setByteCount(long)
+ */
+ public long getByteCount() {
+ return mByteCount;
+ }
+
+ /**
+ * Retrieve the replay bitmap
+ *
+ * <p>This bitmap represents a replay window, allowing the caller to observe out-of-order
+ * delivery. The last bit represents the highest sequence number received so far and bits for
+ * the received packets will be marked as true.
+ *
+ * <p>The size of a replay bitmap will never change over the lifetime of an IpSecTransform
+ *
+ * <p>The replay bitmap is solely useful for inbound IpSecTransforms. For outbound
+ * IpSecTransforms, all bits will be unchecked.
+ *
+ * @see Builder#setReplayBitmap(byte[])
+ */
+ @NonNull
+ public byte[] getReplayBitmap() {
+ return mReplayBitmap.clone();
+ }
+
+ /** Builder class for testing purposes */
+ @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ public static final class Builder {
+ private long mTimeStamp;
+ private long mTxHighestSequenceNumber;
+ private long mRxHighestSequenceNumber;
+ private long mPacketCount;
+ private long mByteCount;
+ private byte[] mReplayBitmap;
+
+ public Builder() {
+ mTimeStamp = System.currentTimeMillis();
+ }
+
+ /**
+ * Set the epoch timestamp (milliseconds) for when this state was created
+ *
+ * @see IpSecTransformState#getTimestamp()
+ */
+ @NonNull
+ public Builder setTimestamp(long timeStamp) {
+ mTimeStamp = timeStamp;
+ return this;
+ }
+
+ /**
+ * Set the highest sequence number sent so far
+ *
+ * @see IpSecTransformState#getTxHighestSequenceNumber()
+ */
+ @NonNull
+ public Builder setTxHighestSequenceNumber(long seqNum) {
+ mTxHighestSequenceNumber = seqNum;
+ return this;
+ }
+
+ /**
+ * Set the highest sequence number received so far
+ *
+ * @see IpSecTransformState#getRxHighestSequenceNumber()
+ */
+ @NonNull
+ public Builder setRxHighestSequenceNumber(long seqNum) {
+ mRxHighestSequenceNumber = seqNum;
+ return this;
+ }
+
+ /**
+ * Set the number of packets received AND sent so far
+ *
+ * @see IpSecTransformState#getPacketCount()
+ */
+ @NonNull
+ public Builder setPacketCount(long packetCount) {
+ mPacketCount = packetCount;
+ return this;
+ }
+
+ /**
+ * Set the number of bytes received AND sent so far
+ *
+ * @see IpSecTransformState#getByteCount()
+ */
+ @NonNull
+ public Builder setByteCount(long byteCount) {
+ mByteCount = byteCount;
+ return this;
+ }
+
+ /**
+ * Set the replay bitmap
+ *
+ * @see IpSecTransformState#getReplayBitmap()
+ */
+ @NonNull
+ public Builder setReplayBitmap(@NonNull byte[] bitMap) {
+ mReplayBitmap = bitMap.clone();
+ return this;
+ }
+
+ /**
+ * Build and validate the IpSecTransformState
+ *
+ * @return an immutable IpSecTransformState instance
+ */
+ @NonNull
+ public IpSecTransformState build() {
+ return new IpSecTransformState(
+ mTimeStamp,
+ mTxHighestSequenceNumber,
+ mRxHighestSequenceNumber,
+ mPacketCount,
+ mByteCount,
+ mReplayBitmap);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index b6f6dbb..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -60,6 +60,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
+import com.android.modules.utils.FastDataInput;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkStatsUtils;
@@ -116,15 +117,28 @@
private long mEndMillis;
private long mTotalBytes;
private boolean mDirty;
+ private final boolean mUseFastDataInput;
/**
* Construct a {@link NetworkStatsCollection} object.
*
- * @param bucketDuration duration of the buckets in this object, in milliseconds.
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
* @hide
*/
public NetworkStatsCollection(long bucketDurationMillis) {
+ this(bucketDurationMillis, false /* useFastDataInput */);
+ }
+
+ /**
+ * Construct a {@link NetworkStatsCollection} object.
+ *
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
+ * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false.
+ * @hide
+ */
+ public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) {
mBucketDurationMillis = bucketDurationMillis;
+ mUseFastDataInput = useFastDataInput;
reset();
}
@@ -483,7 +497,11 @@
/** @hide */
@Override
public void read(InputStream in) throws IOException {
- read((DataInput) new DataInputStream(in));
+ if (mUseFastDataInput) {
+ read(FastDataInput.obtain(in));
+ } else {
+ read((DataInput) new DataInputStream(in));
+ }
}
private void read(DataInput in) throws IOException {
@@ -967,8 +985,8 @@
* @hide
*/
@Nullable
- public static String compareStats(
- NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ public static String compareStats(NetworkStatsCollection migrated,
+ NetworkStatsCollection legacy, boolean allowKeyChange) {
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
migrated.getEntries();
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
@@ -980,7 +998,7 @@
final NetworkStatsHistory legHistory = legEntries.get(legKey);
final NetworkStatsHistory migHistory = migEntries.get(legKey);
- if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
unmatchedLegKeys.remove(legKey);
continue;
}
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..b03eb29 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,7 +28,7 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
void stopDiscovery(int listenerKey);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..053ecf2 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,6 +46,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -57,6 +58,8 @@
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The Network Service Discovery Manager class provides the API to discover services
@@ -150,9 +153,40 @@
public static class Flags {
static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
"com.android.net.flags.register_nsd_offload_engine_api";
+ static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+ "com.android.net.flags.nsd_subtypes_support_enabled";
+ static final String ADVERTISE_REQUEST_API =
+ "com.android.net.flags.advertise_request_api";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a service type specification.
+ *
+ * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+ * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+ * optional comma-separated subtypes.
+ * @hide
+ */
+ public static final String TYPE_REGEX =
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+ // Drop '.' at the end of service type that is compatible with old backend.
+ // e.g. allow "_type._tcp.local."
+ + "\\.?"
+ // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
+ + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
+ + "$";
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -1096,6 +1130,16 @@
return key;
}
+ private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+ final int key;
+ synchronized (mMapLock) {
+ key = getListenerKey(listener);
+ mServiceMap.put(key, s);
+ mExecutorMap.put(key, e);
+ }
+ return key;
+ }
+
private void removeListener(int key) {
synchronized (mMapLock) {
mListenerMap.remove(key);
@@ -1160,14 +1204,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
+ checkServiceInfo(serviceInfo);
+ checkProtocol(protocolType);
+ final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+ protocolType);
+ // Optionally assume that the request is an update request if it uses subtypes and the same
+ // listener. This is not documented behavior as support for advertising subtypes via
+ // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+ // multiple subtypes was broken in T until a later module update. Subtype registration is
+ // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
+ // option for users of the older undocumented behavior, only for subtype changes.
+ if (isSubtypeUpdateRequest(serviceInfo, listener)) {
+ builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ }
+ registerService(builder.build(), executor, listener);
+ }
+
+ private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+ RegistrationListener listener) {
+ // If the listener is the same object, serviceInfo is for the same service name and
+ // type (outside of subtypes), and either of them use subtypes, treat the request as a
+ // subtype update request.
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ if (valueIndex == -1) {
+ return false;
+ }
+ final int key = mListenerMap.keyAt(valueIndex);
+ NsdServiceInfo existingService = mServiceMap.get(key);
+ if (existingService == null) {
+ return false;
+ }
+ final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+ existingService.getServiceType());
+ final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+ serviceInfo.getServiceType());
+ if (existingTypeSubtype == null || newTypeSubtype == null) {
+ return false;
+ }
+ final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+ final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+ if (existingHasNoSubtype && updatedHasNoSubtype) {
+ // Only allow subtype changes when subtypes are used. This ensures that this
+ // behavior does not affect most requests.
+ return false;
+ }
+
+ return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+ && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+ }
+ }
+
+ /**
+ * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+ *
+ * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+ *
+ * @return Type and comma-separated list of subtypes, or null if invalid format.
+ */
+ @Nullable
+ private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
+ final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+ if (!matcher.matches()) return null;
+ // Reject specifications using leading subtypes with a dot
+ if (!TextUtils.isEmpty(matcher.group(1))) return null;
+ return new Pair<>(matcher.group(2), matcher.group(3));
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ * @param advertisingRequest service being registered
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ *
+ * @hide
+ */
+// @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+ public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+ @NonNull Executor executor,
+ @NonNull RegistrationListener listener) {
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+ final int protocolType = advertisingRequest.getProtocolType();
if (serviceInfo.getPort() <= 0) {
throw new IllegalArgumentException("Invalid port number");
}
checkServiceInfo(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ final int key;
+ // For update only request, the old listener has to be reused
+ if ((advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+ key = updateRegisteredListener(listener, executor, serviceInfo);
+ } else {
+ key = putListener(listener, executor, serviceInfo);
+ }
try {
- mService.registerService(key, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index caeecdd..ac4ea23 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,9 @@
package android.net.nsd;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +27,7 @@
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import com.android.net.module.util.InetAddressUtils;
@@ -35,6 +39,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* A class representing service information for network service discovery
@@ -48,9 +53,11 @@
private String mServiceType;
- private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+ private final Set<String> mSubtypes;
- private final List<InetAddress> mHostAddresses = new ArrayList<>();
+ private final ArrayMap<String, byte[]> mTxtRecord;
+
+ private final List<InetAddress> mHostAddresses;
private int mPort;
@@ -60,14 +67,34 @@
private int mInterfaceIndex;
public NsdServiceInfo() {
+ mSubtypes = new ArraySet<>();
+ mTxtRecord = new ArrayMap<>();
+ mHostAddresses = new ArrayList<>();
}
/** @hide */
public NsdServiceInfo(String sn, String rt) {
+ this();
mServiceName = sn;
mServiceType = rt;
}
+ /**
+ * Creates a copy of {@code other}.
+ *
+ * @hide
+ */
+ public NsdServiceInfo(@NonNull NsdServiceInfo other) {
+ mServiceName = other.getServiceName();
+ mServiceType = other.getServiceType();
+ mSubtypes = new ArraySet<>(other.getSubtypes());
+ mTxtRecord = new ArrayMap<>(other.mTxtRecord);
+ mHostAddresses = new ArrayList<>(other.getHostAddresses());
+ mPort = other.getPort();
+ mNetwork = other.getNetwork();
+ mInterfaceIndex = other.getInterfaceIndex();
+ }
+
/** Get the service name */
public String getServiceName() {
return mServiceName;
@@ -391,11 +418,41 @@
mInterfaceIndex = interfaceIndex;
}
+ /**
+ * Sets the subtypes to be advertised for this service instance.
+ *
+ * The elements in {@code subtypes} should be the subtype identifiers which have the trailing
+ * "._sub" removed. For example, the subtype should be "_printer" for
+ * "_printer._sub._http._tcp.local".
+ *
+ * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
+ * case-insensitive value.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ public void setSubtypes(@NonNull Set<String> subtypes) {
+ mSubtypes.clear();
+ mSubtypes.addAll(subtypes);
+ }
+
+ /**
+ * Returns subtypes of this service instance.
+ *
+ * When this object is returned by the service discovery/browse APIs (etc. {@link
+ * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
+ * service.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @NonNull
+ public Set<String> getSubtypes() {
+ return Collections.unmodifiableSet(mSubtypes);
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
+ .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", port: ").append(mPort)
.append(", network: ").append(mNetwork);
@@ -414,6 +471,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mServiceName);
dest.writeString(mServiceType);
+ dest.writeStringList(new ArrayList<>(mSubtypes));
dest.writeInt(mPort);
// TXT record key/value pairs.
@@ -445,6 +503,7 @@
NsdServiceInfo info = new NsdServiceInfo();
info.mServiceName = in.readString();
info.mServiceType = in.readString();
+ info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
info.mPort = in.readInt();
// TXT record key/value pairs.
diff --git a/framework/Android.bp b/framework/Android.bp
index 1e6262d..f3d8689 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -105,7 +105,9 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_library {
@@ -134,7 +136,10 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
- visibility: ["//packages/modules/Connectivity:__subpackages__"]
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_defaults {
@@ -185,10 +190,14 @@
"//packages/modules/Connectivity/Cronet/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
platform_compat_config {
@@ -248,6 +257,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_genrule {
@@ -293,9 +305,9 @@
],
flags: [
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+ "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+ "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
],
aidl: {
include_dirs: [
@@ -308,6 +320,9 @@
java_library {
name: "framework-connectivity-module-api-stubs-including-flagged",
srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Library providing limited APIs within the connectivity module, so that R+ components like
@@ -332,4 +347,7 @@
visibility: [
"//packages/modules/Connectivity/Tethering:__subpackages__",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
index 4ab6d3e..ee422ab 100644
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -36,7 +36,6 @@
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
import android.system.Os;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
@@ -278,13 +277,6 @@
public boolean getDataSaverEnabled() {
throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
- // Note that this is not expected to be called until V given that it relies on the
- // counterpart platform solution to set data saver status to bpf.
- // See {@code NetworkManagementService#setDataSaverModeEnabled}.
- if (!SdkLevel.isAtLeastV()) {
- Log.wtf(TAG, "getDataSaverEnabled is not expected to be called on pre-V devices");
- }
-
try {
return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
} catch (ErrnoException e) {
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a934ddb..fa27d0e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,8 +16,6 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
import static android.net.NetworkRequest.Type.LISTEN;
@@ -27,8 +25,6 @@
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -41,16 +37,12 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.annotation.TargetApi;
-import android.app.Application;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.compat.annotation.UnsupportedAppUsage;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.SocketKeepalive.Callback;
@@ -82,8 +74,6 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -105,7 +95,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class that answers queries about the state of network connectivity. It also
@@ -6262,90 +6251,6 @@
}
/**
- * Helper class to track data saver status.
- *
- * The class will fetch current data saver status from {@link NetworkPolicyManager} when
- * initialized, and listening for status changed intent to cache the latest status.
- *
- * @hide
- */
- @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
- @VisibleForTesting(visibility = PRIVATE)
- public static class DataSaverStatusTracker extends BroadcastReceiver {
- private static final Object sDataSaverStatusTrackerLock = new Object();
-
- private static volatile DataSaverStatusTracker sInstance;
-
- /**
- * Gets a static instance of the class.
- *
- * @param context A {@link Context} for initialization. Note that since the data saver
- * status is global on a device, passing any context is equivalent.
- * @return The static instance of a {@link DataSaverStatusTracker}.
- */
- public static DataSaverStatusTracker getInstance(@NonNull Context context) {
- if (sInstance == null) {
- synchronized (sDataSaverStatusTrackerLock) {
- if (sInstance == null) {
- sInstance = new DataSaverStatusTracker(context);
- }
- }
- }
- return sInstance;
- }
-
- private final NetworkPolicyManager mNpm;
- // The value updates on the caller's binder thread or UI thread.
- private final AtomicBoolean mIsDataSaverEnabled;
-
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public DataSaverStatusTracker(final Context context) {
- // To avoid leaks, take the application context.
- final Context appContext;
- if (context instanceof Application) {
- appContext = context;
- } else {
- appContext = context.getApplicationContext();
- }
-
- if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
- && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
- throw new IllegalStateException("Unexpected caller: "
- + appContext.getApplicationInfo().packageName);
- }
-
- mNpm = appContext.getSystemService(NetworkPolicyManager.class);
- final IntentFilter filter = new IntentFilter(
- ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
- // The receiver should not receive broadcasts from other Apps.
- appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
- mIsDataSaverEnabled = new AtomicBoolean();
- updateDataSaverEnabled();
- }
-
- // Runs on caller's UI thread.
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
- updateDataSaverEnabled();
- } else {
- throw new IllegalStateException("Unexpected intent " + intent);
- }
- }
-
- public boolean getDataSaverEnabled() {
- return mIsDataSaverEnabled.get();
- }
-
- private void updateDataSaverEnabled() {
- // Uid doesn't really matter, but use a fixed UID to make things clearer.
- final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
- mIsDataSaverEnabled.set(dataSaverForCallerUid
- != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
- }
- }
-
- /**
* Return whether the network is blocked for the given uid and metered condition.
*
* Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
@@ -6370,17 +6275,12 @@
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
-
- final boolean isDataSaverEnabled;
- if (SdkLevel.isAtLeastV()) {
- isDataSaverEnabled = reader.getDataSaverEnabled();
- } else {
- final DataSaverStatusTracker dataSaverStatusTracker =
- DataSaverStatusTracker.getInstance(mContext);
- isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
- }
-
- return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+ // Note that before V, the data saver status in bpf is written by ConnectivityService
+ // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+ // the status is not synchronized.
+ // On V+, the data saver status is set by platform code when enabling/disabling
+ // data saver, which is synchronized.
+ return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
}
/** @hide */
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 9824faa..653e41d 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -290,7 +290,10 @@
* Therefore these capabilities are only in NetworkRequest.
*/
private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
- NET_CAPABILITY_LOCAL_NETWORK
+ // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
+ // We cannot currently add it because doing so would crash if the module rolls back,
+ // because JobScheduler persists NetworkRequests to disk, and existing production code
+ // does not consider LOCAL_NETWORK to be a valid capability.
};
private final NetworkCapabilities mNetworkCapabilities;
diff --git a/nearby/README.md b/nearby/README.md
index 8451882..81d2199 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,12 +47,28 @@
## Build and Install
```sh
-$ source build/envsetup.sh && lunch <TARGET>
-$ m com.google.android.tethering.next deapexer
-$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
- ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.next.capex \
- --output /tmp/tethering.apex
-$ adb install -r /tmp/tethering.apex
+For master on AOSP (Android) host
+$ source build/envsetup.sh
+$ lunch aosp_oriole-trunk_staging-userdebug
+$ m com.android.tethering
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/tethering.apex
+$ adb install /tmp/tethering.apex
+$ adb reboot
+
+For udc-mainline-prod on Google internal host
+Build unbundled module using banchan
+$ source build/envsetup.sh
+$ banchan com.google.android.tethering mainline_modules_arm64
+$ m apps_only dist
+$ adb install out/dist/com.google.android.tethering.apex
+$ adb reboot
+Ensure that the module you are installing is compatible with the module currently preloaded on the phone (in /system/apex/com.google.android.tethering.apex). Compatible means:
+
+1. Same package name
+2. Same keys used to sign the apex and the payload
+3. Higher version
+
+See go/mainline-local-build#build-install-local-module for more information
```
## Build and Install from tm-mainline-prod branch
@@ -63,7 +79,7 @@
This is because the device is flashed with AOSP built from master or other branches, which has
prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
built from tm-mainline-prod as below.
-1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+1. adb root && adb remount -R
2. cp tethering.next.apex com.google.android.tethering.apex
3. adb push com.google.android.tethering.apex /system/apex/
4. adb reboot
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 10c1132..8f032bf 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -39,10 +39,15 @@
private final int mKey;
private final byte[] mValue;
- /** @hide */
+ /**
+ * Note this interface is used for internal implementation only.
+ * We only keep those data element types used for encoding and decoding from the specs.
+ * Please read the nearby specs for learning more about each data type and use it as the only
+ * source.
+ *
+ * @hide
+ */
@IntDef({
- DataType.BLE_SERVICE_DATA,
- DataType.BLE_ADDRESS,
DataType.SALT,
DataType.PRIVATE_IDENTITY,
DataType.TRUSTED_IDENTITY,
@@ -50,20 +55,17 @@
DataType.PROVISIONED_IDENTITY,
DataType.TX_POWER,
DataType.ACTION,
- DataType.MODEL_ID,
- DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
DataType.ACCOUNT_KEY_DATA,
DataType.CONNECTION_STATUS,
DataType.BATTERY,
+ DataType.ENCRYPTION_INFO,
+ DataType.BLE_SERVICE_DATA,
+ DataType.BLE_ADDRESS,
DataType.SCAN_MODE,
DataType.TEST_DE_BEGIN,
DataType.TEST_DE_END
})
public @interface DataType {
- int BLE_SERVICE_DATA = 100;
- int BLE_ADDRESS = 101;
- // This is to indicate if the scan is offload only
- int SCAN_MODE = 102;
int SALT = 0;
int PRIVATE_IDENTITY = 1;
int TRUSTED_IDENTITY = 2;
@@ -71,11 +73,19 @@
int PROVISIONED_IDENTITY = 4;
int TX_POWER = 5;
int ACTION = 6;
- int MODEL_ID = 7;
- int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
int ACCOUNT_KEY_DATA = 9;
int CONNECTION_STATUS = 10;
int BATTERY = 11;
+
+ int ENCRYPTION_INFO = 16;
+
+ // Not defined in the spec. Reserved for internal use only.
+ int BLE_SERVICE_DATA = 100;
+ int BLE_ADDRESS = 101;
+ // This is to indicate if the scan is offload only
+ int SCAN_MODE = 102;
+
+ int DEVICE_TYPE = 22;
// Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
// to {@link DataElement.DataType#TEST_DE_END}, inclusive.
// Reserves 128 Test DEs.
@@ -84,27 +94,6 @@
}
/**
- * @hide
- */
- public static boolean isValidType(int type) {
- return type == DataType.BLE_SERVICE_DATA
- || type == DataType.ACCOUNT_KEY_DATA
- || type == DataType.BLE_ADDRESS
- || type == DataType.SCAN_MODE
- || type == DataType.SALT
- || type == DataType.PRIVATE_IDENTITY
- || type == DataType.TRUSTED_IDENTITY
- || type == DataType.PUBLIC_IDENTITY
- || type == DataType.PROVISIONED_IDENTITY
- || type == DataType.TX_POWER
- || type == DataType.ACTION
- || type == DataType.MODEL_ID
- || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
- || type == DataType.CONNECTION_STATUS
- || type == DataType.BATTERY;
- }
-
- /**
* @return {@code true} if this is identity type.
* @hide
*/
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 070a2b6..00f1c38 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -284,6 +284,8 @@
*/
public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<OffloadCapability> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
try {
mService.queryOffloadCapability(new OffloadTransport(executor, callback));
} catch (RemoteException e) {
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 4630902..17b80b0 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -30,7 +30,7 @@
srcs: [":nearby-service-srcs"],
defaults: [
- "framework-system-server-module-defaults"
+ "framework-system-server-module-defaults",
],
libs: [
"androidx.annotation_annotation",
@@ -66,13 +66,16 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-nearby-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
- " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
- " --minApiLevel 33",
+ " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+ " --minApiLevel 33",
out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 9b32d69..9ef905d 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -54,6 +54,11 @@
public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
"nearby_refactor_discovery_manager";
+ /**
+ * Flag to guard enable BLE during Nearby Service init time.
+ */
+ public static final String NEARBY_ENABLE_BLE_IN_INIT = "nearby_enable_ble_in_init";
+
private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -67,6 +72,8 @@
private boolean mSupportTestApp;
@GuardedBy("mDeviceConfigLock")
private boolean mRefactorDiscoveryManager;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mEnableBleInInit;
public NearbyConfiguration() {
mDeviceConfigListener.start();
@@ -131,6 +138,15 @@
}
}
+ /**
+ * @return {@code true} if enableBLE() is called during NearbyService init time.
+ */
+ public boolean enableBleInInit() {
+ synchronized (mDeviceConfigLock) {
+ return mEnableBleInInit;
+ }
+ }
+
private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
public void start() {
DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
@@ -149,6 +165,8 @@
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
mRefactorDiscoveryManager = getDeviceConfigBoolean(
NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+ mEnableBleInInit = getDeviceConfigBoolean(
+ NEARBY_ENABLE_BLE_IN_INIT, true /* defaultValue */);
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 024bff8..28a33fa 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -22,6 +22,7 @@
import android.nearby.BroadcastRequest;
import android.nearby.IBroadcastListener;
import android.nearby.PresenceBroadcastRequest;
+import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -49,6 +50,10 @@
private final NearbyConfiguration mNearbyConfiguration;
private IBroadcastListener mBroadcastListener;
+ // Used with mBroadcastListener. Now we only support single client, for multi clients, a map
+ // between live binder to the information over the binder is needed.
+ // TODO: Finish multi-client logic for broadcast.
+ private BroadcastListenerDeathRecipient mDeathRecipient;
public BroadcastProviderManager(Context context, Injector injector) {
this(ForegroundThread.getExecutor(),
@@ -62,6 +67,7 @@
mLock = new Object();
mNearbyConfiguration = new NearbyConfiguration();
mBroadcastListener = null;
+ mDeathRecipient = null;
}
/**
@@ -70,6 +76,15 @@
public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
synchronized (mLock) {
mExecutor.execute(() -> {
+ if (listener == null) {
+ return;
+ }
+ if (mBroadcastListener != null) {
+ Log.i(TAG, "We do not support multi clients yet,"
+ + " please stop previous broadcast first.");
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
if (!mNearbyConfiguration.isTestAppSupported()) {
NearbyConfiguration configuration = new NearbyConfiguration();
if (!configuration.isPresenceBroadcastLegacyEnabled()) {
@@ -89,7 +104,17 @@
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
+ BroadcastListenerDeathRecipient deathRecipient =
+ new BroadcastListenerDeathRecipient(listener);
+ try {
+ listener.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ // This binder has already died, so call the DeathRecipient as if we had
+ // called linkToDeath in time.
+ deathRecipient.binderDied();
+ }
mBroadcastListener = listener;
+ mDeathRecipient = deathRecipient;
mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
advertisement.toBytes(), this);
});
@@ -113,13 +138,19 @@
*/
public void stopBroadcast(IBroadcastListener listener) {
synchronized (mLock) {
- if (!mNearbyConfiguration.isTestAppSupported()
- && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
- reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
- return;
+ if (listener != null) {
+ if (!mNearbyConfiguration.isTestAppSupported()
+ && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ if (mDeathRecipient != null) {
+ listener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ }
}
mBroadcastListener = null;
- mExecutor.execute(() -> mBleBroadcastProvider.stop());
+ mDeathRecipient = null;
+ mExecutor.execute(mBleBroadcastProvider::stop);
}
}
@@ -142,4 +173,21 @@
Log.e(TAG, "remote exception when reporting status");
}
}
+
+ /**
+ * Class to make listener unregister after the binder is dead.
+ */
+ public class BroadcastListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IBroadcastListener mListener;
+
+ BroadcastListenerDeathRecipient(IBroadcastListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - stop broadcast listener");
+ stopBroadcast(mListener);
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 0c41426..8995232 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -35,6 +36,7 @@
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.managers.registration.DiscoveryRegistration;
import com.android.server.nearby.provider.AbstractDiscoveryProvider;
@@ -66,6 +68,7 @@
private final BleDiscoveryProvider mBleDiscoveryProvider;
private final Injector mInjector;
private final Executor mExecutor;
+ private final NearbyConfiguration mNearbyConfiguration;
public DiscoveryProviderManager(Context context, Injector injector) {
Log.v(TAG, "DiscoveryProviderManager: ");
@@ -75,6 +78,7 @@
mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
new ChreCommunication(injector, mContext, mExecutor), mExecutor);
mInjector = injector;
+ mNearbyConfiguration = new NearbyConfiguration();
}
@VisibleForTesting
@@ -86,6 +90,7 @@
mInjector = injector;
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
+ mNearbyConfiguration = new NearbyConfiguration();
}
private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
@@ -141,6 +146,10 @@
/** Called after boot completed. */
public void init() {
+ // Register BLE only scan when Bluetooth is turned off
+ if (mNearbyConfiguration.enableBleInInit()) {
+ setBleScanEnabled();
+ }
if (mInjector.getContextHubManager() != null) {
mChreDiscoveryProvider.init();
}
@@ -242,7 +251,7 @@
@GuardedBy("mMultiplexerLock")
private void startBleProvider(Set<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ Log.d(TAG, "DiscoveryProviderManager starts BLE scanning.");
mBleDiscoveryProvider.getController().setListener(this);
mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
mBleDiscoveryProvider.getController().setProviderScanFilters(
@@ -313,4 +322,29 @@
public void onMergedRegistrationsUpdated() {
invalidateProviderScanMode();
}
+
+ /**
+ * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+ * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+ * registers Nearby service to Ble scan when Blutooth is off.
+ */
+ public boolean setBleScanEnabled() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ Log.e(TAG, "BluetoothAdapter is null.");
+ return false;
+ }
+ if (adapter.isEnabled() || adapter.isLeEnabled()) {
+ return true;
+ }
+ if (!adapter.isBleScanAlwaysAvailable()) {
+ Log.v(TAG, "Ble always on scan is disabled.");
+ return false;
+ }
+ if (!adapter.enableBLE()) {
+ Log.e(TAG, "Failed to register Ble scan.");
+ return false;
+ }
+ return true;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
index 4b76eba..3ef8fb7 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -38,6 +39,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.metrics.NearbyMetrics;
import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -69,6 +71,7 @@
private final Context mContext;
private final BleDiscoveryProvider mBleDiscoveryProvider;
private final Injector mInjector;
+ private final NearbyConfiguration mNearbyConfiguration;
@ScanRequest.ScanMode
private int mScanMode;
@GuardedBy("mLock")
@@ -83,6 +86,7 @@
mContext, new ChreCommunication(injector, mContext, executor), executor);
mScanTypeScanListenerRecordMap = new HashMap<>();
mInjector = injector;
+ mNearbyConfiguration = new NearbyConfiguration();
Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
}
@@ -96,6 +100,7 @@
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ mNearbyConfiguration = new NearbyConfiguration();
}
private static boolean isChreOnly(List<ScanFilter> scanFilters) {
@@ -142,18 +147,18 @@
for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
continue;
}
CallerIdentity callerIdentity = record.getCallerIdentity();
if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
+ "- not forwarding results");
try {
record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
}
return;
}
@@ -180,7 +185,7 @@
NearbyMetrics.logScanDeviceDiscovered(
record.hashCode(), record.getScanRequest(), nearbyDevice);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onDiscovered.", e);
}
}
}
@@ -193,18 +198,18 @@
for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
continue;
}
CallerIdentity callerIdentity = record.getCallerIdentity();
if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
+ "- not forwarding results");
try {
record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
}
return;
}
@@ -212,7 +217,7 @@
try {
record.getScanListener().onError(errorCode);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onError.", e);
}
}
}
@@ -220,6 +225,10 @@
/** Called after boot completed. */
public void init() {
+ // Register BLE only scan when Bluetooth is turned off
+ if (mNearbyConfiguration.enableBleInInit()) {
+ setBleScanEnabled();
+ }
if (mInjector.getContextHubManager() != null) {
mChreDiscoveryProvider.init();
}
@@ -293,10 +302,10 @@
if (listenerBinder != null && deathRecipient != null) {
listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
}
- Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ Log.v(TAG, "DiscoveryProviderManagerLegacy unregistered scan listener.");
NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
if (mScanTypeScanListenerRecordMap.isEmpty()) {
- Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ Log.v(TAG, "DiscoveryProviderManagerLegacy stops provider because there is no "
+ "scan listener registered.");
stopProviders();
return;
@@ -306,8 +315,8 @@
// Removes current highest scan mode requested and sets the next highest scan mode.
if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
- Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
- + "because the highest scan mode listener was unregistered.");
+ Log.v(TAG, "DiscoveryProviderManagerLegacy starts to find the new highest "
+ + "scan mode because the highest scan mode listener was unregistered.");
@ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
// find the next highest scan mode;
for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
@@ -377,7 +386,7 @@
private void startBleProvider(List<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ Log.d(TAG, "DiscoveryProviderManagerLegacy starts BLE scanning.");
mBleDiscoveryProvider.getController().setListener(this);
mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
@@ -387,7 +396,7 @@
@VisibleForTesting
void startChreProvider(List<ScanFilter> scanFilters) {
- Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ Log.d(TAG, "DiscoveryProviderManagerLegacy starts CHRE scanning.");
mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
mChreDiscoveryProvider.getController().start();
@@ -503,4 +512,29 @@
unregisterScanListener(listener);
}
}
+
+ /**
+ * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+ * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+ * registers Nearby service to Ble scan when Blutooth is off.
+ */
+ public boolean setBleScanEnabled() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ Log.e(TAG, "BluetoothAdapter is null.");
+ return false;
+ }
+ if (adapter.isEnabled() || adapter.isLeEnabled()) {
+ return true;
+ }
+ if (!adapter.isBleScanAlwaysAvailable()) {
+ Log.v(TAG, "Ble always on scan is disabled.");
+ return false;
+ }
+ if (!adapter.enableBLE()) {
+ Log.e(TAG, "Failed to register Ble scan.");
+ return false;
+ }
+ return true;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
new file mode 100644
index 0000000..ac1e18f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.android.server.nearby.presence;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.nearby.DataElement;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.util.Arrays;
+
+/**
+ * Data Element that indicates the presence of extended 16 bytes salt instead of 2 byte salt and to
+ * indicate which encoding scheme (MIC tag or Signature) is used for a section. If present, it must
+ * be the first DE in a section.
+ */
+public class EncryptionInfo {
+
+ @IntDef({EncodingScheme.MIC, EncodingScheme.SIGNATURE})
+ public @interface EncodingScheme {
+ int MIC = 0;
+ int SIGNATURE = 1;
+ }
+
+ // 1st byte : encryption scheme
+ // 2nd to 17th bytes: salt
+ public static final int ENCRYPTION_INFO_LENGTH = 17;
+ private static final int ENCODING_SCHEME_MASK = 0b01111000;
+ private static final int ENCODING_SCHEME_OFFSET = 3;
+ @EncodingScheme
+ private final int mEncodingScheme;
+ private final byte[] mSalt;
+
+ /**
+ * Constructs a {@link DataElement}.
+ */
+ public EncryptionInfo(@NonNull byte[] value) {
+ Preconditions.checkArgument(value.length == ENCRYPTION_INFO_LENGTH);
+ mEncodingScheme = (value[0] & ENCODING_SCHEME_MASK) >> ENCODING_SCHEME_OFFSET;
+ Preconditions.checkArgument(isValidEncodingScheme(mEncodingScheme));
+ mSalt = Arrays.copyOfRange(value, 1, ENCRYPTION_INFO_LENGTH);
+ }
+
+ private boolean isValidEncodingScheme(int scheme) {
+ return scheme == EncodingScheme.MIC || scheme == EncodingScheme.SIGNATURE;
+ }
+
+ @EncodingScheme
+ public int getEncodingScheme() {
+ return mEncodingScheme;
+ }
+
+ public byte[] getSalt() {
+ return mSalt;
+ }
+
+ /** Combines the encoding scheme and salt to a byte array
+ * that represents an {@link EncryptionInfo}.
+ */
+ @Nullable
+ public static byte[] toByte(@EncodingScheme int encodingScheme, byte[] salt) {
+ if (ArrayUtils.isEmpty(salt)) {
+ return null;
+ }
+ if (salt.length != ENCRYPTION_INFO_LENGTH - 1) {
+ return null;
+ }
+ byte schemeByte =
+ (byte) ((encodingScheme << ENCODING_SCHEME_OFFSET) & ENCODING_SCHEME_MASK);
+ return ArrayUtils.append(schemeByte, salt);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
index 34a7514..c2304cc 100644
--- a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -16,22 +16,27 @@
package com.android.server.nearby.presence;
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V1;
+
import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.EncryptionInfo.ENCRYPTION_INFO_LENGTH;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
import android.annotation.Nullable;
-import android.nearby.BroadcastRequest;
+import android.nearby.BroadcastRequest.BroadcastVersion;
import android.nearby.DataElement;
+import android.nearby.DataElement.DataType;
import android.nearby.PresenceBroadcastRequest;
import android.nearby.PresenceCredential;
import android.nearby.PublicCredential;
import android.util.Log;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.encryption.Cryptor;
-import com.android.server.nearby.util.encryption.CryptorImpFake;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -51,37 +56,51 @@
* The header contains:
* version (3 bits) | 5 bit reserved for future use (RFU)
*/
-public class ExtendedAdvertisement extends Advertisement{
+public class ExtendedAdvertisement extends Advertisement {
public static final int SALT_DATA_LENGTH = 2;
-
static final int HEADER_LENGTH = 1;
static final int IDENTITY_DATA_LENGTH = 16;
-
+ // Identity Index is always 2 .
+ // 0 is reserved, 1 is Salt or Credential Element.
+ private static final int CIPHER_START_INDEX = 2;
private final List<DataElement> mDataElements;
+ private final byte[] mKeySeed;
- private final byte[] mAuthenticityKey;
+ private final byte[] mData;
- // All Data Elements including salt and identity.
- // Each list item (byte array) is a Data Element (with its header).
- private final List<byte[]> mCompleteDataElementsBytes;
- // Signature generated from data elements.
- private final byte[] mHmacTag;
+ private ExtendedAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ byte[] keySeed,
+ List<Integer> actions,
+ List<DataElement> dataElements) {
+ this.mVersion = PRESENCE_VERSION_V1;
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mKeySeed = keySeed;
+ this.mDataElements = dataElements;
+ this.mActions = actions;
+ mData = toBytesInternal();
+ }
/**
* Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+ *
* @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
*/
@Nullable
public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
- if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+ if (request.getVersion() != PRESENCE_VERSION_V1) {
Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
return null;
}
byte[] salt = request.getSalt();
- if (salt.length != SALT_DATA_LENGTH) {
+ if (salt.length != SALT_DATA_LENGTH && salt.length != ENCRYPTION_INFO_LENGTH - 1) {
Log.v(TAG, "Salt does not match correct length");
return null;
}
@@ -94,12 +113,12 @@
}
List<Integer> actions = request.getActions();
- if (actions.isEmpty()) {
- Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
- return null;
- }
-
List<DataElement> dataElements = request.getExtendedProperties();
+ // DataElements should include actions.
+ for (int action : actions) {
+ dataElements.add(
+ new DataElement(DataType.ACTION, new byte[]{(byte) action}));
+ }
return new ExtendedAdvertisement(
request.getCredential().getIdentityType(),
identity,
@@ -109,149 +128,252 @@
dataElements);
}
- /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
- @Nullable
- public byte[] toBytes() {
- ByteBuffer buffer = ByteBuffer.allocate(getLength());
-
- // Header
- buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
-
- // Salt
- buffer.put(mCompleteDataElementsBytes.get(0));
-
- // Identity
- buffer.put(mCompleteDataElementsBytes.get(1));
-
- List<Byte> rawDataBytes = new ArrayList<>();
- // Data Elements (Already includes salt and identity)
- for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
- byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
- for (Byte b : dataElementBytes) {
- rawDataBytes.add(b);
- }
- }
-
- byte[] dataElements = new byte[rawDataBytes.size()];
- for (int i = 0; i < rawDataBytes.size(); i++) {
- dataElements[i] = rawDataBytes.get(i);
- }
-
- buffer.put(
- getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
-
- buffer.put(mHmacTag);
-
- return buffer.array();
- }
-
- /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
- * {@code null} when there is something when parsing.
+ /**
+ * Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+ * Return {@code null} when there is an error in parsing.
*/
@Nullable
- public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
- @BroadcastRequest.BroadcastVersion
+ public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential sharedCredential) {
+ @BroadcastVersion
int version = ExtendedAdvertisementUtils.getVersion(bytes);
- if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+ if (version != PRESENCE_VERSION_V1) {
Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
return null;
}
- byte[] authenticityKey = publicCredential.getAuthenticityKey();
-
- int index = HEADER_LENGTH;
- // Salt
- byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
- DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
- if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
- Log.v(TAG, "First data element has to be salt.");
+ byte[] keySeed = sharedCredential.getAuthenticityKey();
+ byte[] metadataEncryptionKeyUnsignedAdvTag = sharedCredential.getEncryptedMetadataKeyTag();
+ if (keySeed == null || metadataEncryptionKeyUnsignedAdvTag == null) {
return null;
}
- index += saltHeaderArray.length;
- byte[] salt = new byte[saltHeader.getDataLength()];
- for (int i = 0; i < saltHeader.getDataLength(); i++) {
- salt[i] = bytes[index++];
- }
- // Identity
+ int index = 0;
+ // Header
+ byte[] header = new byte[]{bytes[index]};
+ index += HEADER_LENGTH;
+ // Section header
+ byte[] sectionHeader = new byte[]{bytes[index]};
+ index += HEADER_LENGTH;
+ // Salt or Encryption Info
+ byte[] firstHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader firstHeader = DataElementHeader.fromBytes(version, firstHeaderArray);
+ if (firstHeader == null) {
+ Log.v(TAG, "Cannot find salt.");
+ return null;
+ }
+ @DataType int firstType = firstHeader.getDataType();
+ if (firstType != DataType.SALT && firstType != DataType.ENCRYPTION_INFO) {
+ Log.v(TAG, "First data element has to be Salt or Encryption Info.");
+ return null;
+ }
+ index += firstHeaderArray.length;
+ byte[] firstDeBytes = new byte[firstHeader.getDataLength()];
+ for (int i = 0; i < firstHeader.getDataLength(); i++) {
+ firstDeBytes[i] = bytes[index++];
+ }
+ byte[] nonce = getNonce(firstType, firstDeBytes);
+ if (nonce == null) {
+ return null;
+ }
+ byte[] saltBytes = firstType == DataType.SALT
+ ? firstDeBytes : (new EncryptionInfo(firstDeBytes)).getSalt();
+
+ // Identity header
byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
DataElementHeader identityHeader =
DataElementHeader.fromBytes(version, identityHeaderArray);
- if (identityHeader == null) {
- Log.v(TAG, "The second element has to be identity.");
+ if (identityHeader == null || identityHeader.getDataLength() != IDENTITY_DATA_LENGTH) {
+ Log.v(TAG, "The second element has to be a 16-bytes identity.");
return null;
}
index += identityHeaderArray.length;
@PresenceCredential.IdentityType int identityType =
toPresenceCredentialIdentityType(identityHeader.getDataType());
- if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
- Log.v(TAG, "The identity type is unknown.");
+ if (identityType != PresenceCredential.IDENTITY_TYPE_PRIVATE
+ && identityType != PresenceCredential.IDENTITY_TYPE_TRUSTED) {
+ Log.v(TAG, "Only supports encrypted advertisement.");
return null;
}
- byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
- for (int i = 0; i < identityHeader.getDataLength(); i++) {
- encryptedIdentity[i] = bytes[index++];
- }
- byte[] identity =
- CryptorImpIdentityV1
- .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
-
- Cryptor cryptor = getCryptor(/* encrypt= */ true);
- byte[] encryptedDataElements =
- new byte[bytes.length - index - cryptor.getSignatureLength()];
- // Decrypt other data elements
- System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
- byte[] decryptedDataElements =
- cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
- if (decryptedDataElements == null) {
+ // Ciphertext
+ Cryptor cryptor = CryptorMicImp.getInstance();
+ byte[] ciphertext = new byte[bytes.length - index - cryptor.getSignatureLength()];
+ System.arraycopy(bytes, index, ciphertext, 0, ciphertext.length);
+ byte[] plaintext = cryptor.decrypt(ciphertext, nonce, keySeed);
+ if (plaintext == null) {
return null;
}
+ // Verification
+ // Verify the computed metadata encryption key tag
+ // First 16 bytes is metadata encryption key data
+ byte[] metadataEncryptionKey = new byte[IDENTITY_DATA_LENGTH];
+ System.arraycopy(plaintext, 0, metadataEncryptionKey, 0, IDENTITY_DATA_LENGTH);
+ // Verify metadata encryption key tag
+ byte[] computedMetadataEncryptionKeyTag =
+ CryptorMicImp.generateMetadataEncryptionKeyTag(metadataEncryptionKey,
+ keySeed);
+ if (!Arrays.equals(computedMetadataEncryptionKeyTag, metadataEncryptionKeyUnsignedAdvTag)) {
+ Log.w(TAG,
+ "The calculated metadata encryption key tag is different from the metadata "
+ + "encryption key unsigned adv tag in the SharedCredential.");
+ return null;
+ }
// Verify the computed HMAC tag is equal to HMAC tag in advertisement
- if (cryptor.getSignatureLength() > 0) {
- byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
- System.arraycopy(
- bytes, bytes.length - cryptor.getSignatureLength(),
- expectedHmacTag, 0, cryptor.getSignatureLength());
- if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
- Log.e(TAG, "HMAC tags not match.");
- return null;
- }
+ byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+ System.arraycopy(
+ bytes, bytes.length - cryptor.getSignatureLength(),
+ expectedHmacTag, 0, cryptor.getSignatureLength());
+ byte[] micInput = ArrayUtils.concatByteArrays(
+ PRESENCE_UUID_BYTES, header, sectionHeader,
+ firstHeaderArray, firstDeBytes,
+ nonce, identityHeaderArray, ciphertext);
+ if (!cryptor.verify(micInput, keySeed, expectedHmacTag)) {
+ Log.e(TAG, "HMAC tag not match.");
+ return null;
}
- int dataElementArrayIndex = 0;
- // Other Data Elements
- List<Integer> actions = new ArrayList<>();
- List<DataElement> dataElements = new ArrayList<>();
- while (dataElementArrayIndex < decryptedDataElements.length) {
- byte[] deHeaderArray = ExtendedAdvertisementUtils
- .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
- DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
- dataElementArrayIndex += deHeaderArray.length;
+ byte[] otherDataElements = new byte[plaintext.length - IDENTITY_DATA_LENGTH];
+ System.arraycopy(plaintext, IDENTITY_DATA_LENGTH,
+ otherDataElements, 0, otherDataElements.length);
+ List<DataElement> dataElements = getDataElementsFromBytes(version, otherDataElements);
+ if (dataElements.isEmpty()) {
+ return null;
+ }
+ List<Integer> actions = getActionsFromDataElements(dataElements);
+ if (actions == null) {
+ return null;
+ }
+ return new ExtendedAdvertisement(identityType, metadataEncryptionKey, saltBytes, keySeed,
+ actions, dataElements);
+ }
- @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
- if (type == DataElement.DataType.ACTION) {
- if (deHeader.getDataLength() != 1) {
- Log.v(TAG, "Action id should only 1 byte.");
+ @PresenceCredential.IdentityType
+ private static int toPresenceCredentialIdentityType(@DataType int type) {
+ switch (type) {
+ case DataType.PRIVATE_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ case DataType.PROVISIONED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+ case DataType.TRUSTED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+ case DataType.PUBLIC_IDENTITY:
+ default:
+ return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+ }
+ }
+
+ @DataType
+ private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+ switch (identityType) {
+ case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+ return DataType.PRIVATE_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+ return DataType.PROVISIONED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+ return DataType.TRUSTED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+ default:
+ return DataType.PUBLIC_IDENTITY;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link DataType} is salt, or one of the
+ * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+ */
+ private static boolean isSaltOrIdentity(@DataType int type) {
+ return type == DataType.SALT || type == DataType.ENCRYPTION_INFO
+ || type == DataType.PRIVATE_IDENTITY
+ || type == DataType.TRUSTED_IDENTITY
+ || type == DataType.PROVISIONED_IDENTITY
+ || type == DataType.PUBLIC_IDENTITY;
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytes() {
+ return mData.clone();
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytesInternal() {
+ int sectionLength = 0;
+ // Salt
+ DataElement saltDe;
+ byte[] nonce;
+ try {
+ switch (mSalt.length) {
+ case SALT_DATA_LENGTH:
+ saltDe = new DataElement(DataType.SALT, mSalt);
+ nonce = CryptorMicImp.generateAdvNonce(mSalt);
+ break;
+ case ENCRYPTION_INFO_LENGTH - 1:
+ saltDe = new DataElement(DataType.ENCRYPTION_INFO,
+ EncryptionInfo.toByte(EncryptionInfo.EncodingScheme.MIC, mSalt));
+ nonce = CryptorMicImp.generateAdvNonce(mSalt, CIPHER_START_INDEX);
+ break;
+ default:
+ Log.w(TAG, "Invalid salt size.");
return null;
- }
- actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
- } else {
- if (isSaltOrIdentity(type)) {
- Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
- + " and one identity in the advertisement.");
- return null;
- }
- byte[] deData = new byte[deHeader.getDataLength()];
- for (int i = 0; i < deHeader.getDataLength(); i++) {
- deData[i] = decryptedDataElements[dataElementArrayIndex++];
- }
- dataElements.add(new DataElement(type, deData));
}
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Failed to generate the IV for encryption.", e);
+ return null;
}
- return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
- dataElements);
+ byte[] saltOrEncryptionInfoBytes =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(saltDe);
+ sectionLength += saltOrEncryptionInfoBytes.length;
+ // 16 bytes encrypted identity
+ @DataType int identityDataType = toDataType(getIdentityType());
+ byte[] identityHeaderBytes = new DataElementHeader(PRESENCE_VERSION_V1,
+ identityDataType, mIdentity.length).toBytes();
+ sectionLength += identityHeaderBytes.length;
+ final List<DataElement> dataElementList = getDataElements();
+ byte[] ciphertext = getCiphertext(nonce, dataElementList);
+ if (ciphertext == null) {
+ return null;
+ }
+ sectionLength += ciphertext.length;
+ // mic
+ sectionLength += CryptorMicImp.MIC_LENGTH;
+ mLength = sectionLength;
+ // header
+ byte header = ExtendedAdvertisementUtils.constructHeader(getVersion());
+ mLength += HEADER_LENGTH;
+ // section header
+ if (sectionLength > 255) {
+ Log.e(TAG, "A section should be shorter than 255 bytes.");
+ return null;
+ }
+ byte sectionHeader = (byte) sectionLength;
+ mLength += HEADER_LENGTH;
+
+ // generates mic
+ ByteBuffer micInputBuffer = ByteBuffer.allocate(
+ mLength + PRESENCE_UUID_BYTES.length + nonce.length - CryptorMicImp.MIC_LENGTH);
+ micInputBuffer.put(PRESENCE_UUID_BYTES);
+ micInputBuffer.put(header);
+ micInputBuffer.put(sectionHeader);
+ micInputBuffer.put(saltOrEncryptionInfoBytes);
+ micInputBuffer.put(nonce);
+ micInputBuffer.put(identityHeaderBytes);
+ micInputBuffer.put(ciphertext);
+ byte[] micInput = micInputBuffer.array();
+ byte[] mic = CryptorMicImp.getInstance().sign(micInput, mKeySeed);
+ if (mic == null) {
+ return null;
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(mLength);
+ buffer.put(header);
+ buffer.put(sectionHeader);
+ buffer.put(saltOrEncryptionInfoBytes);
+ buffer.put(identityHeaderBytes);
+ buffer.put(ciphertext);
+ buffer.put(mic);
+ return buffer.array();
}
/** Returns the {@link DataElement}s in the advertisement. */
@@ -260,7 +382,7 @@
}
/** Returns the {@link DataElement}s in the advertisement according to the key. */
- public List<DataElement> getDataElements(@DataElement.DataType int key) {
+ public List<DataElement> getDataElements(@DataType int key) {
List<DataElement> res = new ArrayList<>();
for (DataElement dataElement : mDataElements) {
if (key == dataElement.getKey()) {
@@ -285,125 +407,86 @@
getActions());
}
- ExtendedAdvertisement(
- @PresenceCredential.IdentityType int identityType,
- byte[] identity,
- byte[] salt,
- byte[] authenticityKey,
- List<Integer> actions,
- List<DataElement> dataElements) {
- this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
- this.mIdentityType = identityType;
- this.mIdentity = identity;
- this.mSalt = salt;
- this.mAuthenticityKey = authenticityKey;
- this.mActions = actions;
- this.mDataElements = dataElements;
- this.mCompleteDataElementsBytes = new ArrayList<>();
+ @Nullable
+ private byte[] getCiphertext(byte[] nonce, List<DataElement> dataElements) {
+ Cryptor cryptor = CryptorMicImp.getInstance();
+ byte[] rawDeBytes = mIdentity;
+ for (DataElement dataElement : dataElements) {
+ rawDeBytes = ArrayUtils.concatByteArrays(rawDeBytes,
+ ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement));
+ }
+ return cryptor.encrypt(rawDeBytes, nonce, mKeySeed);
+ }
- int length = HEADER_LENGTH; // header
+ private static List<DataElement> getDataElementsFromBytes(
+ @BroadcastVersion int version, byte[] bytes) {
+ List<DataElement> res = new ArrayList<>();
+ if (ArrayUtils.isEmpty(bytes)) {
+ return res;
+ }
+ int index = 0;
+ while (index < bytes.length) {
+ byte[] deHeaderArray = ExtendedAdvertisementUtils
+ .getDataElementHeader(bytes, index);
+ DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+ index += deHeaderArray.length;
+ @DataType int type = Objects.requireNonNull(deHeader).getDataType();
+ if (isSaltOrIdentity(type)) {
+ Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+ + " and one identity in the advertisement.");
+ return new ArrayList<>();
+ }
+ byte[] deData = new byte[deHeader.getDataLength()];
+ for (int i = 0; i < deHeader.getDataLength(); i++) {
+ deData[i] = bytes[index++];
+ }
+ res.add(new DataElement(type, deData));
+ }
+ return res;
+ }
- // Salt
- DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
- byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
- mCompleteDataElementsBytes.add(saltByteArray);
- length += saltByteArray.length;
+ @Nullable
+ private static byte[] getNonce(@DataType int type, byte[] data) {
+ try {
+ if (type == DataType.SALT) {
+ if (data.length != SALT_DATA_LENGTH) {
+ Log.v(TAG, "Salt DataElement needs to be 2 bytes.");
+ return null;
+ }
+ return CryptorMicImp.generateAdvNonce(data);
+ } else if (type == DataType.ENCRYPTION_INFO) {
+ try {
+ EncryptionInfo info = new EncryptionInfo(data);
+ if (info.getEncodingScheme() != EncryptionInfo.EncodingScheme.MIC) {
+ Log.v(TAG, "Not support Signature yet.");
+ return null;
+ }
+ return CryptorMicImp.generateAdvNonce(info.getSalt(), CIPHER_START_INDEX);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Salt DataElement needs to be 17 bytes.", e);
+ return null;
+ }
+ }
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Failed to decrypt metadata encryption key.", e);
+ return null;
+ }
+ return null;
+ }
- // Identity
- byte[] encryptedIdentity =
- CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
- DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
- byte[] identityByteArray =
- ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
- mCompleteDataElementsBytes.add(identityByteArray);
- length += identityByteArray.length;
-
- List<Byte> dataElementBytes = new ArrayList<>();
- // Intents
- for (int action : mActions) {
- DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
- new byte[] {(byte) action});
- byte[] intentByteArray =
- ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
- mCompleteDataElementsBytes.add(intentByteArray);
- for (Byte b : intentByteArray) {
- dataElementBytes.add(b);
+ @Nullable
+ private static List<Integer> getActionsFromDataElements(List<DataElement> dataElements) {
+ List<Integer> actions = new ArrayList<>();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() == DataElement.DataType.ACTION) {
+ byte[] value = dataElement.getValue();
+ if (value.length != 1) {
+ Log.w(TAG, "Action should be only 1 byte.");
+ return null;
+ }
+ actions.add(Byte.toUnsignedInt(value[0]));
}
}
-
- // Data Elements (Extended properties)
- for (DataElement dataElement : mDataElements) {
- byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
- mCompleteDataElementsBytes.add(deByteArray);
- for (Byte b : deByteArray) {
- dataElementBytes.add(b);
- }
- }
-
- byte[] data = new byte[dataElementBytes.size()];
- for (int i = 0; i < dataElementBytes.size(); i++) {
- data[i] = dataElementBytes.get(i);
- }
- Cryptor cryptor = getCryptor(/* encrypt= */ true);
- byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
-
- length += encryptedDeBytes.length;
-
- // Signature
- byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
- mHmacTag = hmacTag;
- length += hmacTag.length;
-
- this.mLength = length;
- }
-
- @PresenceCredential.IdentityType
- private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
- switch (type) {
- case DataElement.DataType.PRIVATE_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_PRIVATE;
- case DataElement.DataType.PROVISIONED_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
- case DataElement.DataType.TRUSTED_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_TRUSTED;
- case DataElement.DataType.PUBLIC_IDENTITY:
- default:
- return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
- }
- }
-
- @DataElement.DataType
- private static int toDataType(@PresenceCredential.IdentityType int identityType) {
- switch (identityType) {
- case PresenceCredential.IDENTITY_TYPE_PRIVATE:
- return DataElement.DataType.PRIVATE_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
- return DataElement.DataType.PROVISIONED_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_TRUSTED:
- return DataElement.DataType.TRUSTED_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
- default:
- return DataElement.DataType.PUBLIC_IDENTITY;
- }
- }
-
- /**
- * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
- * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
- */
- private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
- return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
- || type == DataElement.DataType.TRUSTED_IDENTITY
- || type == DataElement.DataType.PROVISIONED_IDENTITY
- || type == DataElement.DataType.PUBLIC_IDENTITY;
- }
-
- private static Cryptor getCryptor(boolean encrypt) {
- if (encrypt) {
- Log.d(TAG, "get V1 Cryptor");
- return CryptorImpV1.getInstance();
- }
- Log.d(TAG, "get fake Cryptor");
- return CryptorImpFake.getInstance();
+ return actions;
}
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
index 54264f7..50dada2 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -18,10 +18,15 @@
import android.os.ParcelUuid;
+import com.android.server.nearby.util.ArrayUtils;
+
/**
* Constants for Nearby Presence operations.
*/
public class PresenceConstants {
+ /** The Presence UUID value in byte array format. */
+ public static final byte[] PRESENCE_UUID_BYTES = ArrayUtils.intToByteArray(0xFCF1);
+
/** Presence advertisement service data uuid. */
public static final ParcelUuid PRESENCE_UUID =
ParcelUuid.fromString("0000fcf1-0000-1000-8000-00805f9b34fb");
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 6829fba..66ae79c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -85,6 +85,7 @@
case BroadcastRequest.PRESENCE_VERSION_V0:
bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
advertiseData, this);
+ Log.v(TAG, "Start to broadcast V0 advertisement.");
break;
case BroadcastRequest.PRESENCE_VERSION_V1:
if (adapter.isLeExtendedAdvertisingSupported()) {
@@ -92,6 +93,7 @@
getAdvertisingSetParameters(),
advertiseData,
null, null, null, mAdvertisingSetCallback);
+ Log.v(TAG, "Start to broadcast V1 advertisement.");
} else {
Log.w(TAG, "Failed to start advertising set because the chipset"
+ " does not supports LE Extended Advertising feature.");
@@ -103,7 +105,8 @@
+ " is wrong.");
advertiseStarted = false;
}
- } catch (NullPointerException | IllegalStateException | SecurityException e) {
+ } catch (NullPointerException | IllegalStateException | SecurityException
+ | IllegalArgumentException e) {
Log.w(TAG, "Failed to start advertising.", e);
advertiseStarted = false;
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 355f7cf..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -45,7 +45,6 @@
import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
@@ -69,15 +68,6 @@
@GuardedBy("mLock")
@Nullable
private List<android.nearby.ScanFilter> mScanFilters;
- private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
- new android.bluetooth.le.ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult scanResult) {
- }
- @Override
- public void onScanFailed(int errorCode) {
- }
- };
private android.bluetooth.le.ScanCallback mScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
@@ -133,10 +123,6 @@
.addMedium(NearbyDevice.Medium.BLE)
.setName(deviceName)
.setRssi(rssi);
- for (int i : advertisement.getActions()) {
- builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
- new byte[]{(byte) i}));
- }
for (DataElement dataElement : advertisement.getDataElements()) {
builder.addExtendedProperty(dataElement);
}
@@ -175,7 +161,6 @@
if (isBleAvailable()) {
Log.d(TAG, "BleDiscoveryProvider started");
startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
- startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
return;
}
Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -192,7 +177,6 @@
}
Log.v(TAG, "Ble scan stopped.");
bluetoothLeScanner.stopScan(mScanCallback);
- bluetoothLeScanner.stopScan(mScanCallbackLegacy);
synchronized (mLock) {
if (mScanFilters != null) {
mScanFilters = null;
@@ -284,17 +268,13 @@
if (advertisement == null) {
continue;
}
- if (CryptorImpIdentityV1.getInstance().verify(
- advertisement.getIdentity(),
- credential.getEncryptedMetadataKeyTag())) {
- builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
- rssi));
- builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
- if (!ArrayUtils.isEmpty(credential.getSecretId())) {
- builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
- }
- return;
+ builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+ rssi));
+ builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+ builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
}
+ return;
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index d69d42d..21ec252 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,6 +20,9 @@
import static com.android.server.nearby.NearbyService.TAG;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -155,7 +158,7 @@
builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
try {
callback.onQueryComplete(builder.build());
- } catch (RemoteException e) {
+ } catch (RemoteException | NullPointerException e) {
e.printStackTrace();
}
});
@@ -350,7 +353,11 @@
DataElement.DataType.ACTION,
new byte[]{(byte) filterResult.getIntent()}));
}
-
+ if (filterResult.hasTimestampNs()) {
+ presenceDeviceBuilder
+ .setDiscoveryTimestampMillis(MILLISECONDS.convert(
+ filterResult.getTimestampNs(), NANOSECONDS));
+ }
PublicCredential publicCredential =
new PublicCredential.Builder(
secretId,
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 35251d8..e69d004 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -22,6 +22,10 @@
* ArrayUtils class that help manipulate array.
*/
public class ArrayUtils {
+ private static final char[] HEX_UPPERCASE = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
/** Concatenate N arrays of bytes into a single array. */
public static byte[] concatByteArrays(byte[]... arrays) {
// Degenerate case - no input provided.
@@ -52,4 +56,67 @@
public static boolean isEmpty(byte[] bytes) {
return bytes == null || bytes.length == 0;
}
+
+ /** Appends a byte array to a byte. */
+ public static byte[] append(byte a, byte[] b) {
+ if (b == null) {
+ return new byte[]{a};
+ }
+
+ int length = b.length;
+ byte[] result = new byte[length + 1];
+ result[0] = a;
+ System.arraycopy(b, 0, result, 1, length);
+ return result;
+ }
+
+ /**
+ * Converts an Integer to a 2-byte array.
+ */
+ public static byte[] intToByteArray(int value) {
+ return new byte[] {(byte) (value >> 8), (byte) value};
+ }
+
+ /** Appends a byte to a byte array. */
+ public static byte[] append(byte[] a, byte b) {
+ if (a == null) {
+ return new byte[]{b};
+ }
+
+ int length = a.length;
+ byte[] result = new byte[length + 1];
+ System.arraycopy(a, 0, result, 0, length);
+ result[length] = b;
+ return result;
+ }
+
+ /** Convert an hex string to a byte array. */
+
+ public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+ int length = hex.length();
+ if (length % 2 != 0) {
+ throw new IllegalArgumentException("Hex string has odd number of characters");
+ }
+ byte[] out = new byte[length / 2];
+ for (int i = 0; i < length; i += 2) {
+ // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+ // our hex values are in 0, 255.
+ out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+ }
+ return out;
+ }
+
+ /** Encodes a byte array as a hexadecimal representation of bytes. */
+ public static String bytesToStringUppercase(byte[] bytes) {
+ int length = bytes.length;
+ StringBuilder out = new StringBuilder(length * 2);
+ for (int i = 0; i < length; i++) {
+ if (i == length - 1 && (bytes[i] & 0xff) == 0) {
+ break;
+ }
+ out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+ out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+ }
+ return out.toString();
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
index 3c5132d..ba9ca41 100644
--- a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -22,6 +22,10 @@
import androidx.annotation.Nullable;
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -30,22 +34,28 @@
/** Class for encryption/decryption functionality. */
public abstract class Cryptor {
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ public static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ public static final byte[] NP_HKDF_SALT = "Google Nearby".getBytes(StandardCharsets.US_ASCII);
/** AES only supports key sizes of 16, 24 or 32 bytes. */
static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
- private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+ public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
/**
* Encrypt the provided data blob.
*
* @param data data blob to be encrypted.
- * @param salt used for IV
+ * @param iv advertisement nonce
* @param secretKeyBytes secrete key accessed from credentials
* @return encrypted data, {@code null} if failed to encrypt.
*/
@Nullable
- public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+ public byte[] encrypt(byte[] data, byte[] iv, byte[] secretKeyBytes) {
return data;
}
@@ -53,12 +63,12 @@
* Decrypt the original data blob from the provided byte array.
*
* @param encryptedData data blob to be decrypted.
- * @param salt used for IV
+ * @param iv advertisement nonce
* @param secretKeyBytes secrete key accessed from credentials
* @return decrypted data, {@code null} if failed to decrypt.
*/
@Nullable
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+ public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] secretKeyBytes) {
return encryptedData;
}
@@ -90,7 +100,7 @@
* A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
* given size.
*/
- // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+ // Based on crypto/tink/subtle/Hkdf.java
@Nullable
static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
Mac mac;
@@ -146,4 +156,66 @@
return result;
}
+
+ /**
+ * Computes an HKDF.
+ *
+ * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+ * "HMACSHA256".
+ * @param ikm the input keying material.
+ * @param salt optional salt. A possibly non-secret random value.
+ * (If no salt is provided i.e. if salt has length 0)
+ * then an array of 0s of the same size as the hash
+ * digest is used as salt.
+ * @param info optional context and application specific information.
+ * @param size The length of the generated pseudorandom string in bytes.
+ * @throws GeneralSecurityException if the {@code macAlgorithm} is not supported or if {@code
+ * size} is too large or if {@code salt} is not a valid key for
+ * macAlgorithm (which should not
+ * happen since HMAC allows key sizes up to 2^64).
+ */
+ public static byte[] computeHkdf(
+ String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)
+ throws GeneralSecurityException {
+ Mac mac = Mac.getInstance(macAlgorithm);
+ if (size > 255 * mac.getMacLength()) {
+ throw new GeneralSecurityException("size too large");
+ }
+ if (salt == null || salt.length == 0) {
+ // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+ // then HKDF uses a salt that is an array of zeros of the same length as the hash
+ // digest.
+ mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+ } else {
+ mac.init(new SecretKeySpec(salt, macAlgorithm));
+ }
+ byte[] prk = mac.doFinal(ikm);
+ byte[] result = new byte[size];
+ int ctr = 1;
+ int pos = 0;
+ mac.init(new SecretKeySpec(prk, macAlgorithm));
+ byte[] digest = new byte[0];
+ while (true) {
+ mac.update(digest);
+ mac.update(info);
+ mac.update((byte) ctr);
+ digest = mac.doFinal();
+ if (pos + digest.length < size) {
+ System.arraycopy(digest, 0, result, pos, digest.length);
+ pos += digest.length;
+ ctr++;
+ } else {
+ System.arraycopy(digest, 0, result, pos, size - pos);
+ break;
+ }
+ }
+ return result;
+ }
+
+ /** Generates the HMAC bytes. */
+ public static byte[] generateHmac(String algorithm, byte[] input, byte[] key) {
+ return Hashing.hmacSha256(new SecretKeySpec(key, algorithm))
+ .hashBytes(input)
+ .asBytes();
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
deleted file mode 100644
index 1c0ec9e..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
+++ /dev/null
@@ -1,39 +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.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import androidx.annotation.Nullable;
-
-/**
- * A Cryptor that returns the original data without actual encryption
- */
-public class CryptorImpFake extends Cryptor {
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable
- private static CryptorImpFake sCryptor;
-
- /** Returns an instance of CryptorImpFake. */
- public static CryptorImpFake getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpFake();
- }
- return sCryptor;
- }
-
- private CryptorImpFake() {
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
deleted file mode 100644
index b0e19b4..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
+++ /dev/null
@@ -1,208 +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.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
- * encryption and decryption.
- */
-public class CryptorImpIdentityV1 extends Cryptor {
-
- // 3 16 byte arrays known by both the encryptor and decryptor.
- private static final byte[] EK_IV =
- new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
- private static final byte[] ESALT_IV =
- new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
- private static final byte[] KTAG_IV =
- {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
-
- /** Length of encryption key required by AES/GCM encryption. */
- private static final int ENCRYPTION_KEY_SIZE = 32;
-
- /** Length of salt required by AES/GCM encryption. */
- private static final int AES_CTR_IV_SIZE = 16;
-
- /** Length HMAC tag */
- public static final int HMAC_TAG_SIZE = 8;
-
- /**
- * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
- */
- private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
- @VisibleForTesting
- static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable private static CryptorImpIdentityV1 sCryptor;
-
- /** Returns an instance of CryptorImpIdentityV1. */
- public static CryptorImpIdentityV1 getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpIdentityV1();
- }
- return sCryptor;
- }
-
- @Nullable
- @Override
- public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Encrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
- if (esalt == null) {
- Log.e(TAG, "Failed to generate salt.");
- return null;
- }
- try {
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
- try {
- return cipher.doFinal(data);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- }
-
- @Nullable
- @Override
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Decrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to get cipher instance.", e);
- return null;
- }
- byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
- if (esalt == null) {
- return null;
- }
- try {
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
-
- try {
- return cipher.doFinal(encryptedData);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
- return null;
- }
- }
-
- /**
- * Generates a digital signature for the data.
- *
- * @return signature {@code null} if failed to sign
- */
- @Nullable
- @Override
- public byte[] sign(byte[] data, byte[] salt) {
- if (data == null) {
- Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
- return null;
- }
-
- // Generates a 8 bytes HMAC tag
- return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
- }
-
- /**
- * Generates a digital signature for the data.
- * Uses KTAG_IV as salt value.
- */
- @Nullable
- public byte[] sign(byte[] data) {
- // Generates a 8 bytes HMAC tag
- return sign(data, KTAG_IV);
- }
-
- @Override
- public boolean verify(byte[] data, byte[] key, byte[] signature) {
- return Arrays.equals(sign(data, key), signature);
- }
-
- /**
- * Verifies the signature generated by data and key, with the original signed data. Uses
- * KTAG_IV as salt value.
- */
- public boolean verify(byte[] data, byte[] signature) {
- return verify(data, KTAG_IV, signature);
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
deleted file mode 100644
index 15073fb..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
+++ /dev/null
@@ -1,212 +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.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
- */
-public class CryptorImpV1 extends Cryptor {
-
- /**
- * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
- */
- private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
- @VisibleForTesting
- static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
- /** Length of encryption key required by AES/GCM encryption. */
- private static final int ENCRYPTION_KEY_SIZE = 32;
-
- /** Length of salt required by AES/GCM encryption. */
- private static final int AES_CTR_IV_SIZE = 16;
-
- /** Length HMAC tag */
- public static final int HMAC_TAG_SIZE = 16;
-
- // 3 16 byte arrays known by both the encryptor and decryptor.
- private static final byte[] AK_IV =
- new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
- private static final byte[] ASALT_IV =
- new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
- private static final byte[] HK_IV =
- new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable private static CryptorImpV1 sCryptor;
-
- /** Returns an instance of CryptorImpV1. */
- public static CryptorImpV1 getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpV1();
- }
- return sCryptor;
- }
-
- private CryptorImpV1() {
- }
-
- @Nullable
- @Override
- public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Encrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
- if (asalt == null) {
- Log.e(TAG, "Failed to generate salt.");
- return null;
- }
- try {
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
- try {
- return cipher.doFinal(data);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- }
-
- @Nullable
- @Override
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Decrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to get cipher instance.", e);
- return null;
- }
- byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
- if (asalt == null) {
- return null;
- }
- try {
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
-
- try {
- return cipher.doFinal(encryptedData);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
- return null;
- }
- }
-
- @Override
- @Nullable
- public byte[] sign(byte[] data, byte[] key) {
- return generateHmacTag(data, key);
- }
-
- @Override
- public int getSignatureLength() {
- return HMAC_TAG_SIZE;
- }
-
- @Override
- public boolean verify(byte[] data, byte[] key, byte[] signature) {
- return Arrays.equals(sign(data, key), signature);
- }
-
- /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
- * is equal to HMAC tag in advertisement to see data integrity. */
- @Nullable
- @VisibleForTesting
- byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
- if (data == null || authenticityKey == null) {
- Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
- return null;
- }
-
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.e(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes HMAC key from authenticity_key
- byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
- if (hmacKey == null) {
- Log.e(TAG, "Failed to generate HMAC key.");
- return null;
- }
-
- // Generates a 16 bytes HMAC tag from authenticity_key
- return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
new file mode 100644
index 0000000..3dbf85c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * MIC encryption and decryption for {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1}
+ * advertisement
+ */
+public class CryptorMicImp extends Cryptor {
+
+ public static final int MIC_LENGTH = 16;
+
+ private static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+ private static final byte[] AES_KEY_INFO_BYTES = "Unsigned Section AES key".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final byte[] ADV_NONCE_INFO_BYTES_SALT_DE = "Unsigned Section IV".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final byte[] ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE =
+ "V1 derived salt".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] METADATA_KEY_HMAC_KEY_INFO_BYTES =
+ "Unsigned Section metadata key HMAC key".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] MIC_HMAC_KEY_INFO_BYTES = "Unsigned Section HMAC key".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final int AES_KEY_SIZE = 16;
+ private static final int ADV_NONCE_SIZE_SALT_DE = 16;
+ private static final int ADV_NONCE_SIZE_ENCRYPTION_INFO_DE = 12;
+ private static final int HMAC_KEY_SIZE = 32;
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable
+ private static CryptorMicImp sCryptor;
+
+ private CryptorMicImp() {
+ }
+
+ /** Returns an instance of CryptorImpV1. */
+ public static CryptorMicImp getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorMicImp();
+ }
+ return sCryptor;
+ }
+
+ /**
+ * Generate the meta data encryption key tag
+ * @param metadataEncryptionKey used as identity
+ * @param keySeed authenticity key saved in local and shared credential
+ * @return bytes generated by hmac or {@code null} when there is an error
+ */
+ @Nullable
+ public static byte[] generateMetadataEncryptionKeyTag(byte[] metadataEncryptionKey,
+ byte[] keySeed) {
+ try {
+ byte[] metadataKeyHmacKey = generateMetadataKeyHmacKey(keySeed);
+ return Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+ metadataEncryptionKey, /* key= */ metadataKeyHmacKey);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failed to generate Metadata encryption key tag.", e);
+ return null;
+ }
+ }
+
+ /**
+ * @param salt from the 2 bytes Salt Data Element
+ */
+ @Nullable
+ public static byte[] generateAdvNonce(byte[] salt) throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ salt,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ ADV_NONCE_INFO_BYTES_SALT_DE,
+ /* size= */ ADV_NONCE_SIZE_SALT_DE);
+ }
+
+ /** Generates the 12 bytes nonce with salt from the 2 bytes Salt Data Element */
+ @Nullable
+ public static byte[] generateAdvNonce(byte[] salt, int deIndex)
+ throws GeneralSecurityException {
+ // go/nearby-specs-working-doc
+ // Indices are encoded as big-endian unsigned 32-bit integers, starting at 1.
+ // Index 0 is reserved
+ byte[] indexBytes = new byte[4];
+ indexBytes[3] = (byte) deIndex;
+ byte[] info =
+ ArrayUtils.concatByteArrays(ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE, indexBytes);
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ salt,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ info,
+ /* size= */ ADV_NONCE_SIZE_ENCRYPTION_INFO_DE);
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] input, byte[] iv, byte[] keySeed) {
+ if (input == null || iv == null || keySeed == null) {
+ return null;
+ }
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+
+ byte[] aesKey;
+ try {
+ aesKey = generateAesKey(keySeed);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Encryption failed because failed to generate the AES key.", e);
+ return null;
+ }
+ if (aesKey == null) {
+ Log.i(TAG, "Failed to generate the AES key.");
+ return null;
+ }
+ SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+
+ }
+ try {
+ return cipher.doFinal(input);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] keySeed) {
+ if (encryptedData == null || iv == null || keySeed == null) {
+ return null;
+ }
+
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] aesKey;
+ try {
+ aesKey = generateAesKey(keySeed);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Decryption failed because failed to generate the AES key.", e);
+ return null;
+ }
+ SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ byte[] res = generateHmacTag(data, key);
+ return res;
+ }
+
+ @Override
+ public int getSignatureLength() {
+ return MIC_LENGTH;
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /**
+ * Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+ * is equal to HMAC tag in advertisement to see data integrity.
+ *
+ * @param input concatenated advertisement UUID, header, section header, derived salt, and
+ * section content
+ * @param keySeed the MIC HMAC key is calculated using the derived key
+ * @return the first 16 bytes of HMAC-SHA256 result
+ */
+ @Nullable
+ @VisibleForTesting
+ byte[] generateHmacTag(byte[] input, byte[] keySeed) {
+ try {
+ if (input == null || keySeed == null) {
+ return null;
+ }
+ byte[] micHmacKey = generateMicHmacKey(keySeed);
+ byte[] hmac = Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+ input, /* key= */ micHmacKey);
+ if (ArrayUtils.isEmpty(hmac)) {
+ return null;
+ }
+ return Arrays.copyOf(hmac, MIC_LENGTH);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failed to generate mic hmac key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ private static byte[] generateAesKey(byte[] keySeed) throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ keySeed,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ AES_KEY_INFO_BYTES,
+ /* size= */ AES_KEY_SIZE);
+ }
+
+ private static byte[] generateMetadataKeyHmacKey(byte[] keySeed)
+ throws GeneralSecurityException {
+ return generateHmacKey(keySeed, METADATA_KEY_HMAC_KEY_INFO_BYTES);
+ }
+
+ private static byte[] generateMicHmacKey(byte[] keySeed) throws GeneralSecurityException {
+ return generateHmacKey(keySeed, MIC_HMAC_KEY_INFO_BYTES);
+ }
+
+ private static byte[] generateHmacKey(byte[] keySeed, byte[] info)
+ throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ keySeed,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ info,
+ /* size= */ HMAC_KEY_SIZE);
+ }
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index e1bf455..bf9357b 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -115,6 +115,9 @@
repeated DataElement data_element = 7;
optional bytes ble_service_data = 8;
optional ResultType result_type = 9;
+ // Timestamp when the device is discovered, in nanoseconds,
+ // relative to Android SystemClock.elapsedRealtimeNanos().
+ optional uint64 timestamp_ns = 10;
}
message BleFilterResults {
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..4309d7e 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -39,6 +39,7 @@
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
certificate: "platform",
sdk_version: "module_current",
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index 5ddfed3..644e178 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -25,6 +25,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
import android.provider.DeviceConfig;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -68,7 +69,12 @@
"3", false);
Thread.sleep(500);
- assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ // TestAppSupported Flag can only be set to true in user-debug devices.
+ if (Build.isDebuggable()) {
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ } else {
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+ }
assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index bc38210..7ff7b13 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -24,7 +24,9 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.UiAutomation;
import android.content.Context;
@@ -34,6 +36,7 @@
import android.nearby.PresenceBroadcastRequest;
import android.nearby.PresenceCredential;
import android.nearby.PrivateCredential;
+import android.os.IBinder;
import android.provider.DeviceConfig;
import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@
IBroadcastListener mBroadcastListener;
@Mock
BleBroadcastProvider mBleBroadcastProvider;
+ @Mock
+ IBinder mBinder;
private Context mContext;
private BroadcastProviderManager mBroadcastProviderManager;
private BroadcastRequest mBroadcastRequest;
@@ -82,9 +87,12 @@
@Before
public void setUp() {
+ when(mBroadcastListener.asBinder()).thenReturn(mBinder);
mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
DeviceConfig.setProperty(
NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
mContext = ApplicationProvider.getApplicationContext();
mBroadcastProviderManager = new BroadcastProviderManager(
@@ -104,15 +112,28 @@
}
@Test
- public void testStartAdvertising() {
+ public void testStartAdvertising() throws Exception {
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+ verify(mBinder).linkToDeath(any(), eq(0));
}
@Test
public void testStopAdvertising() {
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
+ verify(mBinder).unlinkToDeath(any(), eq(0));
+ }
+
+ @Test
+ public void testRegisterAdvertising_twoTimes_fail() throws Exception {
+ IBroadcastListener newListener = mock(IBroadcastListener.class);
+ IBinder newBinder = mock(IBinder.class);
+ when(newListener.asBinder()).thenReturn(newBinder);
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, newListener);
+ verify(newListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
index aa0dad3..0ca571a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.managers;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -23,10 +24,12 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -36,6 +39,8 @@
import android.nearby.ScanRequest;
import android.os.IBinder;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.provider.BleDiscoveryProvider;
import com.android.server.nearby.provider.ChreCommunication;
@@ -86,6 +91,8 @@
DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
@Mock
IBinder mIBinder;
+ @Mock
+ BluetoothAdapter mBluetoothAdapter;
private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
mScanTypeScanListenerRecordMap;
@@ -135,10 +142,15 @@
@Before
public void setup() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG);
MockitoAnnotations.initMocks(this);
when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
mScanTypeScanListenerRecordMap = new HashMap<>();
mDiscoveryProviderManager =
@@ -164,6 +176,13 @@
}
@Test
+ public void test_enableBleWhenBleOff() throws Exception {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ mDiscoveryProviderManager.init();
+ verify(mBluetoothAdapter, times(1)).enableBLE();
+ }
+
+ @Test
public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -375,4 +394,62 @@
.isTrue();
assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
}
+
+ @Test
+ public void isBluetoothEnabledTest_bluetoothEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void isBluetoothEnabledTest_bleEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enableFailed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
+
+ @Test
+ public void enabledTest_scanIsOn() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_failed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 7ecf631..7cea34a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.managers;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -24,10 +25,12 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -37,6 +40,8 @@
import android.nearby.ScanRequest;
import android.os.IBinder;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.provider.BleDiscoveryProvider;
import com.android.server.nearby.provider.ChreCommunication;
@@ -80,6 +85,8 @@
CallerIdentity mCallerIdentity;
@Mock
IBinder mIBinder;
+ @Mock
+ BluetoothAdapter mBluetoothAdapter;
private Executor mExecutor;
private DiscoveryProviderManager mDiscoveryProviderManager;
@@ -128,12 +135,17 @@
@Before
public void setup() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG);
MockitoAnnotations.initMocks(this);
mExecutor = Executors.newSingleThreadExecutor();
when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
when(mScanListener.asBinder()).thenReturn(mIBinder);
+ when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
mDiscoveryProviderManager =
new DiscoveryProviderManager(mContext, mExecutor, mInjector,
@@ -157,6 +169,13 @@
}
@Test
+ public void test_enableBleWhenBleOff() throws Exception {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ mDiscoveryProviderManager.init();
+ verify(mBluetoothAdapter, times(1)).enableBLE();
+ }
+
+ @Test
public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
reset(mBluetoothController);
when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -336,4 +355,62 @@
.isTrue();
assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
}
+
+ @Test
+ public void isBluetoothEnabledTest_bluetoothEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void isBluetoothEnabledTest_bleEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enableFailed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
+
+ @Test
+ public void enabledTest_scanIsOn() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_failed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
new file mode 100644
index 0000000..6ec7c57
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.server.nearby.presence.EncryptionInfo.EncodingScheme;
+import com.android.server.nearby.util.ArrayUtils;
+
+import org.junit.Test;
+
+
+/**
+ * Unit test for {@link EncryptionInfo}.
+ */
+public class EncryptionInfoTest {
+ private static final byte[] SALT =
+ new byte[]{25, -21, 35, -108, -26, -126, 99, 60, 110, 45, -116, 34, 91, 126, -23, 127};
+
+ @Test
+ public void test_illegalLength() {
+ byte[] data = new byte[]{1, 2};
+ assertThrows(IllegalArgumentException.class, () -> new EncryptionInfo(data));
+ }
+
+ @Test
+ public void test_illegalEncodingScheme() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new EncryptionInfo(ArrayUtils.append((byte) 0b10110000, SALT)));
+ assertThrows(IllegalArgumentException.class,
+ () -> new EncryptionInfo(ArrayUtils.append((byte) 0b01101000, SALT)));
+ }
+
+ @Test
+ public void test_getMethods_signature() {
+ byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+
+ @Test
+ public void test_getMethods_mic() {
+ byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+ @Test
+ public void test_toBytes() {
+ byte[] data = EncryptionInfo.toByte(EncodingScheme.MIC, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
index 895df69..3f00a42 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.presence;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
+
import static com.google.common.truth.Truth.assertThat;
import android.nearby.BroadcastRequest;
@@ -25,19 +27,22 @@
import android.nearby.PrivateCredential;
import android.nearby.PublicCredential;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
import org.junit.Before;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ExtendedAdvertisementTest {
+ private static final int EXTENDED_ADVERTISEMENT_BYTE_LENGTH = 67;
private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final int DATA_TYPE_ACTION = 6;
private static final int DATA_TYPE_MODEL_ID = 7;
private static final int DATA_TYPE_BLE_ADDRESS = 101;
private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
@@ -49,18 +54,23 @@
private static final DataElement BLE_ADDRESS_ELEMENT =
new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
- private static final byte[] IDENTITY =
- new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final byte[] METADATA_ENCRYPTION_KEY =
+ new byte[]{-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
private static final int MEDIUM_TYPE_BLE = 0;
private static final byte[] SALT = {2, 3};
+
private static final int PRESENCE_ACTION_1 = 1;
private static final int PRESENCE_ACTION_2 = 2;
+ private static final DataElement PRESENCE_ACTION_DE_1 =
+ new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_1});
+ private static final DataElement PRESENCE_ACTION_DE_2 =
+ new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_2});
private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
private static final byte[] AUTHENTICITY_KEY =
new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
private static final byte[] PUBLIC_KEY =
- new byte[] {
+ new byte[]{
48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
-83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
@@ -68,7 +78,7 @@
123, 41, -119, -25, 1, -112, 112
};
private static final byte[] ENCRYPTED_METADATA_BYTES =
- new byte[] {
+ new byte[]{
-44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
-18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
@@ -78,23 +88,65 @@
-4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
};
private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
- new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+ new byte[]{-100, 102, -35, -99, 66, -85, -55, -58, -52, 11, -74, 102, 109, -89, 1, -34,
+ 45, 43, 107, -60, 99, -21, 28, 34, 31, -100, -96, 108, 108, -18, 107, 5};
+
+ private static final String ENCODED_ADVERTISEMENT_ENCRYPTION_INFO =
+ "2091911000DE2A89ED98474AF3E41E48487E8AEBDE90014C18BCB9F9AAC5C11A1BE00A10A5DCD2C49A74BE"
+ + "BAF0FE72FD5053B9DF8B9976C80BE0DCE8FEE83F1BFA9A89EB176CA48EE4ED5D15C6CDAD6B9E"
+ + "41187AA6316D7BFD8E454A53971AC00836F7AB0771FF0534050037D49C6AEB18CF9F8590E5CD"
+ + "EE2FBC330FCDC640C63F0735B7E3F02FE61A0496EF976A158AD3455D";
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG_2 =
+ new byte[]{-54, -39, 41, 16, 61, 79, -116, 14, 94, 0, 84, 45, 26, -108, 66, -48, 124,
+ -81, 61, 56, -98, -47, 14, -19, 116, 106, -27, 123, -81, 49, 83, -42};
+
private static final String DEVICE_NAME = "test_device";
+ private static final byte[] SALT_16 =
+ ArrayUtils.stringToBytes("DE2A89ED98474AF3E41E48487E8AEBDE");
+ private static final byte[] AUTHENTICITY_KEY_2 = ArrayUtils.stringToBytes(
+ "959D2F3CAB8EE4A2DEB0255C03762CF5D39EB919300420E75A089050FB025E20");
+ private static final byte[] METADATA_ENCRYPTION_KEY_2 = ArrayUtils.stringToBytes(
+ "EF5E9A0867560E52AE1F05FCA7E48D29");
+
+ private static final DataElement DE1 = new DataElement(571, ArrayUtils.stringToBytes(
+ "537F96FD94E13BE589F0141145CFC0EEC4F86FBDB2"));
+ private static final DataElement DE2 = new DataElement(541, ArrayUtils.stringToBytes(
+ "D301FFB24B5B"));
+ private static final DataElement DE3 = new DataElement(51, ArrayUtils.stringToBytes(
+ "EA95F07C25B75C04E1B2B8731F6A55BA379FB141"));
+ private static final DataElement DE4 = new DataElement(729, ArrayUtils.stringToBytes(
+ "2EFD3101E2311BBB108F0A7503907EAF0C2EAAA60CDA8D33A294C4CEACE0"));
+ private static final DataElement DE5 = new DataElement(411, ArrayUtils.stringToBytes("B0"));
+
private PresenceBroadcastRequest.Builder mBuilder;
+ private PresenceBroadcastRequest.Builder mBuilderCredentialInfo;
private PrivateCredential mPrivateCredential;
+ private PrivateCredential mPrivateCredential2;
+
private PublicCredential mPublicCredential;
+ private PublicCredential mPublicCredential2;
@Before
public void setUp() {
mPrivateCredential =
- new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mPrivateCredential2 =
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY_2, METADATA_ENCRYPTION_KEY_2, DEVICE_NAME)
.setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
.build();
mPublicCredential =
new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
.build();
+ mPublicCredential2 =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY_2, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG_2)
+ .build();
mBuilder =
new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
SALT, mPrivateCredential)
@@ -103,6 +155,16 @@
.addAction(PRESENCE_ACTION_2)
.addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
.addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+
+ mBuilderCredentialInfo =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT_16, mPrivateCredential2)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addExtendedProperty(DE1)
+ .addExtendedProperty(DE2)
+ .addExtendedProperty(DE3)
+ .addExtendedProperty(DE4)
+ .addExtendedProperty(DE5);
}
@Test
@@ -112,36 +174,50 @@
assertThat(originalAdvertisement.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(originalAdvertisement.getLength()).isEqualTo(66);
assertThat(originalAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
assertThat(originalAdvertisement.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2,
+ MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
+ }
+
+ @Test
+ public void test_createFromRequest_credentialInfo() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilderCredentialInfo.build());
+
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT_16);
+ assertThat(originalAdvertisement.getDataElements())
+ .containsExactly(DE1, DE2, DE3, DE4, DE5);
}
@Test
public void test_createFromRequest_encodeAndDecode() {
ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
mBuilder.build());
-
byte[] generatedBytes = originalAdvertisement.toBytes();
-
ExtendedAdvertisement newAdvertisement =
ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
assertThat(newAdvertisement.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(newAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(newAdvertisement.getLength()).isEqualTo(66);
+ assertThat(newAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
assertThat(newAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
assertThat(newAdvertisement.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+ PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
}
@Test
@@ -170,45 +246,77 @@
.setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
.addAction(PRESENCE_ACTION_1);
assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
-
- // empty action
- PresenceBroadcastRequest.Builder builder3 =
- new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
- SALT, mPrivateCredential)
- .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
- assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
}
@Test
- public void test_toBytes() {
+ public void test_toBytesSalt() throws Exception {
ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
}
@Test
- public void test_fromBytes() {
+ public void test_fromBytesSalt() throws Exception {
byte[] originalBytes = getExtendedAdvertisementByteArray();
ExtendedAdvertisement adv =
ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
assertThat(adv.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(adv.getLength()).isEqualTo(66);
+ assertThat(adv.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
assertThat(adv.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(adv.getSalt()).isEqualTo(SALT);
assertThat(adv.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+ PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
+ }
+
+ @Test
+ public void test_toBytesCredentialElement() {
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.createFromRequest(mBuilderCredentialInfo.build());
+ assertThat(ArrayUtils.bytesToStringUppercase(adv.toBytes())).isEqualTo(
+ ENCODED_ADVERTISEMENT_ENCRYPTION_INFO);
+ }
+
+ @Test
+ public void test_fromBytesCredentialElement() {
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(
+ ArrayUtils.stringToBytes(ENCODED_ADVERTISEMENT_ENCRYPTION_INFO),
+ mPublicCredential2);
+ assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+ assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(adv.getVersion()).isEqualTo(BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(adv.getSalt()).isEqualTo(SALT_16);
+ assertThat(adv.getDataElements()).containsExactly(DE1, DE2, DE3, DE4, DE5);
+ }
+
+ @Test
+ public void test_fromBytes_metadataTagNotMatched_fail() throws Exception {
+ byte[] originalBytes = getExtendedAdvertisementByteArray();
+ PublicCredential credential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES,
+ new byte[]{113, 90, -55, 73, 25, -9, 55, -44, 102, 44, 81, -68, 101, 21, 32,
+ 92, -107, 3, 108, 90, 28, -73, 16, 49, -95, -121, 8, -45, -27, 16,
+ 6, 108})
+ .build();
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(originalBytes, credential);
+ assertThat(adv).isNull();
}
@Test
public void test_toString() {
ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
- + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
- + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+ + "<VERSION: 1, length: " + EXTENDED_ADVERTISEMENT_BYTE_LENGTH
+ + ", dataElementCount: 4, identityType: 1, "
+ + "identity: " + Arrays.toString(METADATA_ENCRYPTION_KEY)
+ + ", salt: [2, 3],"
+ " actions: [1, 2]>");
}
@@ -222,26 +330,22 @@
assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
}
- private static byte[] getExtendedAdvertisementByteArray() {
- ByteBuffer buffer = ByteBuffer.allocate(66);
+ private static byte[] getExtendedAdvertisementByteArray() throws Exception {
+ CryptorMicImp cryptor = CryptorMicImp.getInstance();
+ ByteBuffer buffer = ByteBuffer.allocate(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
buffer.put((byte) 0b00100000); // Header V1
- buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+ buffer.put(
+ (byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)); // Section header (section length)
+
// Salt data
- buffer.put(SALT);
+ // Salt header: length 2, type 0
+ byte[] saltBytes = ArrayUtils.concatByteArrays(new byte[]{(byte) 0b00100000}, SALT);
+ buffer.put(saltBytes);
// Identity header: length 16, type 1 (private identity)
- buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
- // Identity data
- buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+ byte[] identityHeader = new byte[]{(byte) 0b10010000, (byte) 0b00000001};
+ buffer.put(identityHeader);
ByteBuffer deBuffer = ByteBuffer.allocate(28);
- // Action1 header: length 1, type 6
- deBuffer.put(new byte[]{(byte) 0b00010110});
- // Action1 data
- deBuffer.put((byte) PRESENCE_ACTION_1);
- // Action2 header: length 1, type 6
- deBuffer.put(new byte[]{(byte) 0b00010110});
- // Action2 data
- deBuffer.put((byte) PRESENCE_ACTION_2);
// Ble address header: length 7, type 102
deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
// Ble address data
@@ -250,11 +354,30 @@
deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
// model id data
deBuffer.put(MODE_ID_DATA);
+ // Action1 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action1 data
+ deBuffer.put((byte) PRESENCE_ACTION_1);
+ // Action2 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action2 data
+ deBuffer.put((byte) PRESENCE_ACTION_2);
+ byte[] deBytes = deBuffer.array();
+ byte[] nonce = CryptorMicImp.generateAdvNonce(SALT);
+ byte[] ciphertext =
+ cryptor.encrypt(
+ ArrayUtils.concatByteArrays(METADATA_ENCRYPTION_KEY, deBytes),
+ nonce, AUTHENTICITY_KEY);
+ buffer.put(ciphertext);
- byte[] data = deBuffer.array();
- CryptorImpV1 cryptor = CryptorImpV1.getInstance();
- buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
- buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+ byte[] dataToSign = ArrayUtils.concatByteArrays(
+ PRESENCE_UUID_BYTES, /* UUID */
+ new byte[]{(byte) 0b00100000}, /* header */
+ new byte[]{(byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)} /* sectionHeader */,
+ saltBytes, /* salt */
+ nonce, identityHeader, ciphertext);
+ byte[] mic = cryptor.sign(dataToSign, AUTHENTICITY_KEY);
+ buffer.put(mic);
return buffer.array();
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..0b1a742 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.provider;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
mBleBroadcastProvider.onStartSuccess(settings);
- verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
}
@Test
@@ -81,7 +82,7 @@
// advertising set can not be mocked, so we will allow nulls
mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
AdvertisingSetCallback.ADVERTISE_SUCCESS);
- verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..590a46e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -192,7 +192,12 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testOnNearbyDeviceDiscoveredWithDataElements() {
+ public void testOnNearbyDeviceDiscoveredWithDataElements_TIME() {
+ // The feature only supports user-debug builds.
+ if (!Build.isDebuggable()) {
+ return;
+ }
+
// Disables the setting of test app support
boolean isSupportedTestApp = getDeviceConfigBoolean(
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
@@ -209,6 +214,7 @@
// First byte is length of service data, padding zeros should be thrown away.
final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ final long timestampNs = 1697765417070000000L;
final List<DataElement> expectedExtendedProperties = new ArrayList<>();
expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
@@ -262,6 +268,7 @@
.setValue(ByteString.copyFrom(testData))
.setValueLength(testData.length)
)
+ .setTimestampNs(timestampNs)
.build();
Blefilter.BleFilterResults results =
Blefilter.BleFilterResults.newBuilder().addResult(result).build();
@@ -285,11 +292,18 @@
DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
}
+ // Nanoseconds to Milliseconds
+ assertThat((mNearbyDevice.getValue().getPresenceDevice())
+ .getDiscoveryTimestampMillis()).isEqualTo(timestampNs / 1000000);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+ // The feature only supports user-debug builds.
+ if (!Build.isDebuggable()) {
+ return;
+ }
// Enables the setting of test app support
boolean isSupportedTestApp = getDeviceConfigBoolean(
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
index a759baf..455c432 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -18,8 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.filters.SdkSuppress;
-
import org.junit.Test;
public final class ArrayUtilsTest {
@@ -30,51 +28,58 @@
private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysNoInput() {
assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysOneEmptyArray() {
assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysOneNonEmptyArray() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysMultipleNonEmptyArrays() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysMultipleArrays() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
.isEqualTo(BYTES_ALL);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmptyNull_returnsTrue() {
assertThat(ArrayUtils.isEmpty(null)).isTrue();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmpty_returnsTrue() {
assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmpty_returnsFalse() {
assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
}
+
+ @Test
+ public void testAppendByte() {
+ assertThat(ArrayUtils.append((byte) 2, BYTES_ONE)).isEqualTo(new byte[]{2, 7, 9});
+ }
+
+ @Test
+ public void testAppendByteNull() {
+ assertThat(ArrayUtils.append((byte) 2, null)).isEqualTo(new byte[]{2});
+ }
+
+ @Test
+ public void testAppendByteToArray() {
+ assertThat(ArrayUtils.append(BYTES_ONE, (byte) 2)).isEqualTo(new byte[]{7, 9, 2});
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
deleted file mode 100644
index f0294fc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
+++ /dev/null
@@ -1,76 +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.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-public class CryptorImpIdentityV1Test {
- private static final String TAG = "CryptorImpIdentityV1Test";
- private static final byte[] SALT = new byte[] {102, 22};
- private static final byte[] DATA =
- new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
- private static final byte[] AUTHENTICITY_KEY =
- new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
- @Test
- public void test_encrypt_decrypt() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
- }
-
- @Test
- public void test_encryption() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- // for debugging
- Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
- assertThat(encryptedData).isEqualTo(getEncryptedData());
- }
-
- @Test
- public void test_decryption() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] decryptedData =
- identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
- // for debugging
- Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
- assertThat(decryptedData).isEqualTo(DATA);
- }
-
- @Test
- public void generateHmacTag() {
- CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] generatedTag = identityCryptor.sign(DATA);
- byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
- assertThat(generatedTag).isEqualTo(expectedTag);
- }
-
- private static byte[] getEncryptedData() {
- return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
- }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
deleted file mode 100644
index 3ca2575..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
+++ /dev/null
@@ -1,114 +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.
- */
-
-package com.android.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit test for {@link CryptorImpV1}
- */
-public final class CryptorImpV1Test {
- private static final String TAG = "CryptorImpV1Test";
- private static final byte[] SALT = new byte[] {102, 22};
- private static final byte[] DATA =
- new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
- private static final byte[] AUTHENTICITY_KEY =
- new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
- @Test
- public void test_encryption() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- // for debugging
- Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
- assertThat(encryptedData).isEqualTo(getEncryptedData());
- }
-
- @Test
- public void test_encryption_invalidInput() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
- }
-
- @Test
- public void test_decryption() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- byte[] decryptedData =
- v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
- // for debugging
- Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
- assertThat(decryptedData).isEqualTo(DATA);
- }
-
- @Test
- public void test_decryption_invalidInput() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
- }
-
- @Test
- public void generateSign() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
- byte[] expectedTag = new byte[]{
- 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
- assertThat(generatedTag).isEqualTo(expectedTag);
- }
-
- @Test
- public void test_verify() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] expectedTag = new byte[]{
- 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-
- assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
- assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
- }
-
- @Test
- public void test_generateHmacTag_sameResult() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
- assertThat(res1)
- .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
- }
-
- @Test
- public void test_generateHmacTag_nullData() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
- }
-
- @Test
- public void test_generateHmacTag_nullKey() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
- }
-
- private static byte[] getEncryptedData() {
- return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
- }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
new file mode 100644
index 0000000..b6d2333
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+/**
+ * Unit test for {@link CryptorMicImp}
+ */
+public final class CryptorMicImpTest {
+ private static final String TAG = "CryptorImpV1Test";
+ private static final byte[] SALT = new byte[]{102, 22};
+ private static final byte[] DATA =
+ new byte[]{107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[]{-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{112, 23, -111, 87, 122, -27, 45, -25, -35, 84, -89, 115, 61, 113};
+ }
+
+ @Test
+ public void test_encryption() throws Exception {
+ Cryptor v1Cryptor = CryptorMicImp.getInstance();
+ byte[] encryptedData =
+ v1Cryptor.encrypt(DATA, CryptorMicImp.generateAdvNonce(SALT), AUTHENTICITY_KEY);
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_decryption() throws Exception {
+ Cryptor v1Cryptor = CryptorMicImp.getInstance();
+ byte[] decryptedData =
+ v1Cryptor.decrypt(getEncryptedData(), CryptorMicImp.generateAdvNonce(SALT),
+ AUTHENTICITY_KEY);
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_verify() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ byte[] expectedTag = new byte[]{
+ -80, -51, -101, -7, -65, 110, 37, 68, 122, -128, 57, -90, -115, -59, -61, 46};
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+ }
+
+ @Test
+ public void test_generateHmacTag_sameResult() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+ assertThat(res1)
+ .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+ }
+
+ @Test
+ public void test_generateHmacTag_nullData() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+ }
+
+ @Test
+ public void test_generateHmacTag_nullKey() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
index ca612e3..1fb2236 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -42,7 +42,7 @@
assertThat(res2).hasLength(outputSize);
assertThat(res1).isNotEqualTo(res2);
assertThat(res1)
- .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+ .isEqualTo(CryptorMicImp.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
}
@Test
diff --git a/netd/Android.bp b/netd/Android.bp
index 4325d89..3cdbc97 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -69,10 +69,10 @@
"BpfBaseTest.cpp"
],
static_libs: [
+ "libbase",
"libnetd_updatable",
],
shared_libs: [
- "libbase",
"libcutils",
"liblog",
"libnetdutils",
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 8b9e5a7..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
android::netdutils::Status ret = sBpfHandler.init(cg2_path);
if (!android::netdutils::isOk(ret)) {
- LOG(ERROR) << __func__ << ": Failed. " << ret.code() << " " << ret.msg();
- return -ret.code();
+ LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+ abort();
}
return 0;
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..de879f3 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -31,6 +31,7 @@
],
visibility: ["//visibility:private"],
}
+
// The above filegroup can be used to specify different sources depending
// on the branch, while minimizing merge conflicts in the rest of the
// build rules.
@@ -78,6 +79,9 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Test building mDNS as a standalone, so that it can be imported into other repositories as-is.
@@ -94,11 +98,12 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
":framework-connectivity-t-mdns-standalone-build-sources",
- ":service-mdns-droidstubs"
+ ":service-mdns-droidstubs",
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
@@ -127,7 +132,7 @@
srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
libs: [
"net-utils-device-common-mdns-standalone-build-test",
- "service-connectivity-tiramisu-pre-jarjar"
+ "service-connectivity-tiramisu-pre-jarjar",
],
visibility: [
"//visibility:private",
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 0dfd0af..b9f3adb 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -73,6 +73,7 @@
"-Wthread-safety",
],
static_libs: [
+ "libbase",
"libgmock",
"libnetworkstats",
"libperfetto_client_experimental",
@@ -80,7 +81,6 @@
"perfetto_trace_protos",
],
shared_libs: [
- "libbase",
"liblog",
"libcutils",
"libandroid_net",
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
new file mode 100644
index 0000000..3ed21a2
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -0,0 +1,171 @@
+/*
+ * 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 com.android.metrics;
+
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to log NetworkStats related metrics.
+ *
+ * This class does not provide thread-safe.
+ */
+public class NetworkStatsMetricsLogger {
+ final Dependencies mDeps;
+ int mReadIndex = 1;
+
+ /** Dependency class */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Writes a NETWORK_STATS_RECORDER_FILE_OPERATION_REPORTED event to ConnectivityStatsLog.
+ */
+ public void writeRecorderFileReadingStats(int recorderType, int readIndex,
+ int readLatencyMillis,
+ int fileCount, int totalFileSize,
+ int keys, int uids, int totalHistorySize,
+ boolean useFastDataInput) {
+ ConnectivityStatsLog.write(NETWORK_STATS_RECORDER_FILE_OPERATED,
+ NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ,
+ recorderType,
+ readIndex,
+ readLatencyMillis,
+ fileCount,
+ totalFileSize,
+ keys,
+ uids,
+ totalHistorySize,
+ useFastDataInput
+ ? NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED
+ : NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED);
+ }
+ }
+
+ public NetworkStatsMetricsLogger() {
+ mDeps = new Dependencies();
+ }
+
+ @VisibleForTesting
+ public NetworkStatsMetricsLogger(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ private static int prefixToRecorderType(@NonNull String prefix) {
+ switch (prefix) {
+ case PREFIX_XT:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+ case PREFIX_UID:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+ case PREFIX_UID_TAG:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+ default:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+ }
+ }
+
+ /**
+ * Get file count and total byte count for the given directory and prefix.
+ *
+ * @return File count and total byte count as a pair, or 0s if met errors.
+ */
+ private static Pair<Integer, Integer> getStatsFilesAttributes(
+ @Nullable File statsDir, @NonNull String prefix) {
+ if (statsDir == null) return new Pair<>(0, 0);
+
+ // Only counts the matching files.
+ // The files are named in the following format:
+ // <prefix>.<startTimestamp>-[<endTimestamp>]
+ // e.g. uid_tag.12345-
+ // See FileRotator#FileInfo for more detail.
+ final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
+
+ // Ensure that base path exists.
+ statsDir.mkdirs();
+
+ int totalFiles = 0;
+ int totalBytes = 0;
+ for (String name : emptyIfNull(statsDir.list())) {
+ if (!pattern.matcher(name).matches()) continue;
+
+ totalFiles++;
+ // Cast to int is safe since stats persistent files are several MBs in total.
+ totalBytes += (int) (new File(statsDir, name).length());
+
+ }
+ return new Pair<>(totalFiles, totalBytes);
+ }
+
+ private static String [] emptyIfNull(@Nullable String [] array) {
+ return (array == null) ? new String[0] : array;
+ }
+
+ /**
+ * Log statistics from the NetworkStatsRecorder file reading process into statsd.
+ */
+ public void logRecorderFileReading(@NonNull String prefix, int readLatencyMillis,
+ @Nullable File statsDir, @NonNull NetworkStatsCollection collection,
+ boolean useFastDataInput) {
+ final Set<Integer> uids = new HashSet<>();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> entries =
+ collection.getEntries();
+
+ for (final NetworkStatsCollection.Key key : entries.keySet()) {
+ uids.add(key.uid);
+ }
+
+ int totalHistorySize = 0;
+ for (final NetworkStatsHistory history : entries.values()) {
+ totalHistorySize += history.size();
+ }
+
+ final Pair<Integer, Integer> fileAttributes = getStatsFilesAttributes(statsDir, prefix);
+ mDeps.writeRecorderFileReadingStats(prefixToRecorderType(prefix),
+ mReadIndex++,
+ readLatencyMillis,
+ fileAttributes.first /* fileCount */,
+ fileAttributes.second /* totalFileSize */,
+ entries.size(),
+ uids.size(),
+ totalHistorySize,
+ useFastDataInput);
+ }
+}
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index a884840..ea91e64 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -42,6 +42,7 @@
import android.net.IpSecSpiResponse;
import android.net.IpSecTransform;
import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
import android.net.IpSecTunnelInterfaceResponse;
import android.net.IpSecUdpEncapResponse;
import android.net.LinkAddress;
@@ -70,6 +71,7 @@
import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
import libcore.io.IoUtils;
@@ -109,6 +111,7 @@
@VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
private final INetd mNetd;
+ private final IpSecXfrmController mIpSecXfrmCtrl;
static {
try {
@@ -152,6 +155,11 @@
}
return netd;
}
+
+ /** Get a instance of IpSecXfrmController */
+ public IpSecXfrmController getIpSecXfrmController() {
+ return new IpSecXfrmController();
+ }
}
final UidFdTagger mUidFdTagger;
@@ -1111,6 +1119,7 @@
mContext = context;
mDeps = Objects.requireNonNull(deps, "Missing dependencies.");
mUidFdTagger = uidFdTagger;
+ mIpSecXfrmCtrl = mDeps.getIpSecXfrmController();
try {
mNetd = mDeps.getNetdInstance(mContext);
} catch (RemoteException e) {
@@ -1862,6 +1871,48 @@
releaseResource(userRecord.mTransformRecords, resourceId);
}
+ @Override
+ public synchronized IpSecTransformState getTransformState(int transformId)
+ throws IllegalStateException, RemoteException {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NETWORK_STATE, "IpsecService#getTransformState");
+
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ TransformRecord transformInfo =
+ userRecord.mTransformRecords.getResourceOrThrow(transformId);
+
+ final int spi = transformInfo.getSpiRecord().getSpi();
+ final InetAddress destAddress =
+ InetAddresses.parseNumericAddress(
+ transformInfo.getConfig().getDestinationAddress());
+ Log.d(TAG, "getTransformState for spi " + spi + " destAddress " + destAddress);
+
+ // Make netlink call
+ final XfrmNetlinkNewSaMessage xfrmNewSaMsg;
+ try {
+ xfrmNewSaMsg = mIpSecXfrmCtrl.ipSecGetSa(destAddress, Integer.toUnsignedLong(spi));
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "getTransformState: failed to get IpSecTransformState" + e.toString());
+ throw new IllegalStateException("Failed to get IpSecTransformState", e);
+ }
+
+ // Keep the netlink socket open to save time for the next call. It is cheap to have a
+ // persistent netlink socket in the system server
+
+ if (xfrmNewSaMsg == null) {
+ Log.e(TAG, "getTransformState: failed to get IpSecTransformState xfrmNewSaMsg is null");
+ throw new IllegalStateException("Failed to get IpSecTransformState");
+ }
+
+ return new IpSecTransformState.Builder()
+ .setTxHighestSequenceNumber(xfrmNewSaMsg.getTxSequenceNumber())
+ .setRxHighestSequenceNumber(xfrmNewSaMsg.getRxSequenceNumber())
+ .setPacketCount(xfrmNewSaMsg.getPacketCount())
+ .setByteCount(xfrmNewSaMsg.getByteCount())
+ .setReplayBitmap(xfrmNewSaMsg.getBitmap())
+ .build();
+ }
+
/**
* Apply an active transport mode transform to a socket, which will apply the IPsec security
* association as a correspondent policy to the provided socket
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
new file mode 100644
index 0000000..c8abd40
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -0,0 +1,198 @@
+/*
+ * 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 com.android.server;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkGetSaMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * This class handles IPSec XFRM commands between IpSecService and the Linux kernel
+ *
+ * <p>Synchronization in IpSecXfrmController is done on all entrypoints due to potential race
+ * conditions at the kernel/xfrm level.
+ */
+public class IpSecXfrmController {
+ private static final String TAG = IpSecXfrmController.class.getSimpleName();
+
+ private static final boolean VDBG = false; // STOPSHIP: if true
+
+ private static final int TIMEOUT_MS = 500;
+ private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+
+ @NonNull private final Dependencies mDependencies;
+ @Nullable private FileDescriptor mNetlinkSocket;
+
+ @VisibleForTesting
+ public IpSecXfrmController(@NonNull Dependencies dependencies) {
+ mDependencies = dependencies;
+ }
+
+ public IpSecXfrmController() {
+ this(new Dependencies());
+ }
+
+ /**
+ * Start the XfrmController
+ *
+ * <p>The method is idempotent
+ */
+ public synchronized void openNetlinkSocketIfNeeded() throws ErrnoException, SocketException {
+ if (mNetlinkSocket == null) {
+ mNetlinkSocket = mDependencies.newNetlinkSocket();
+ }
+ }
+
+ /**
+ * Stop the XfrmController
+ *
+ * <p>The method is idempotent
+ */
+ public synchronized void closeNetlinkSocketIfNeeded() {
+ if (mNetlinkSocket != null) {
+ mDependencies.releaseNetlinkSocket(mNetlinkSocket);
+ mNetlinkSocket = null;
+ }
+ }
+
+ @VisibleForTesting
+ public synchronized FileDescriptor getNetlinkSocket() {
+ return mNetlinkSocket;
+ }
+
+ /** Dependencies of IpSecXfrmController, for injection in tests. */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get a new XFRM netlink socket and connect it */
+ public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM);
+ NetlinkUtils.connectToKernel(fd);
+ return fd;
+ }
+
+ /** Close the netlink socket */
+ // TODO: b/205923322 This annotation is to suppress the lint error complaining that
+ // #closeQuietly requires Android S. It can be removed when the infra supports setting
+ // service-connectivity min_sdk to 31
+ @TargetApi(Build.VERSION_CODES.S)
+ public void releaseNetlinkSocket(FileDescriptor fd) {
+ IoUtils.closeQuietly(fd);
+ }
+
+ /** Send a netlink message to a socket */
+ public void sendMessage(FileDescriptor fd, byte[] bytes)
+ throws ErrnoException, InterruptedIOException {
+ NetlinkUtils.sendMessage(fd, bytes, 0, bytes.length, TIMEOUT_MS);
+ }
+
+ /** Receive a netlink message from a socket */
+ public ByteBuffer recvMessage(FileDescriptor fd)
+ throws ErrnoException, InterruptedIOException {
+ return NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+ }
+ }
+
+ @GuardedBy("IpSecXfrmController.this")
+ private NetlinkMessage sendRequestAndGetResponse(String methodTag, byte[] req)
+ throws ErrnoException, InterruptedIOException, IOException {
+ openNetlinkSocketIfNeeded();
+
+ logD(methodTag + ": send request " + req.length + " bytes");
+ logV(HexDump.dumpHexString(req));
+ mDependencies.sendMessage(mNetlinkSocket, req);
+
+ final ByteBuffer response = mDependencies.recvMessage(mNetlinkSocket);
+ logD(methodTag + ": receive response " + response.limit() + " bytes");
+ logV(HexDump.dumpHexString(response.array(), 0 /* offset */, response.limit()));
+
+ final NetlinkMessage msg = XfrmNetlinkMessage.parse(response, NETLINK_XFRM);
+ if (msg == null) {
+ throw new IOException("Fail to parse the response message");
+ }
+
+ final int msgType = msg.getHeader().nlmsg_type;
+ if (msgType == NetlinkConstants.NLMSG_ERROR) {
+ final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
+ final int errorCode = errorMsg.getNlMsgError().error;
+ throw new ErrnoException(methodTag, errorCode);
+ }
+
+ return msg;
+ }
+
+ /** Get the state of an IPsec SA */
+ @NonNull
+ public synchronized XfrmNetlinkNewSaMessage ipSecGetSa(
+ @NonNull final InetAddress destAddress, long spi)
+ throws ErrnoException, InterruptedIOException, IOException {
+ logD("ipSecGetSa: destAddress=" + destAddress + " spi=" + spi);
+
+ final byte[] req =
+ XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(
+ destAddress, spi, (short) IPPROTO_ESP);
+ try {
+ final NetlinkMessage msg = sendRequestAndGetResponse("ipSecGetSa", req);
+
+ final int messageType = msg.getHeader().nlmsg_type;
+ if (messageType != XFRM_MSG_NEWSA) {
+ throw new IOException("unexpected response type " + messageType);
+ }
+
+ return (XfrmNetlinkNewSaMessage) msg;
+ } catch (IllegalArgumentException exception) {
+ // Maybe thrown from Struct.parse
+ throw new IOException("Failed to parse the response " + exception);
+ }
+ }
+
+ private static void logV(String details) {
+ if (VDBG) {
+ Log.v(TAG, details);
+ }
+ }
+
+ private static void logD(String details) {
+ Log.d(TAG, details);
+ }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 7f869a7..cc9d53d 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,8 @@
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -51,6 +53,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -111,7 +114,10 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -170,6 +176,8 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
+
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
"mdns_config_running_app_active_importance_cutoff";
@@ -186,6 +194,7 @@
static final int NO_TRANSACTION = -1;
private static final int NO_SENT_QUERY_COUNT = 0;
private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
+ private static final int MAX_SUBTYPE_COUNT = 100;
private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
private final Context mContext;
@@ -688,17 +697,45 @@
return mClients.get(args.connector);
}
+ /**
+ * Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
+ * contains invalid subtype label.
+ */
+ private boolean checkSubtypeLabels(Set<String> subtypes) {
+ if (subtypes.size() > MAX_SUBTYPE_COUNT) {
+ mServiceLogs.e(
+ "Too many subtypes: " + subtypes.size() + " (max = "
+ + MAX_SUBTYPE_COUNT + ")");
+ return false;
+ }
+
+ for (String subtype : subtypes) {
+ if (!checkSubtypeLabel(subtype)) {
+ mServiceLogs.e("Subtype " + subtype + " is invalid");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
+ final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
+ for (String subtype : subtypes) {
+ subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+ }
+ return new ArraySet<>(subtypeMap.values());
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -716,14 +753,14 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
+ final Pair<String, List<String>> typeAndSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
- if (serviceType == null) {
+ if (serviceType == null || typeAndSubtype.second.size() > 1) {
clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
@@ -738,10 +775,11 @@
.setNetwork(info.getNetwork())
.setRemoveExpiredService(true)
.setIsPassiveMode(true);
- if (typeAndSubtype.second != null) {
+ if (!typeAndSubtype.second.isEmpty()) {
// The parsing ensures subtype starts with an underscore.
// MdnsSearchOptions expects the underscore to not be present.
- optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+ optionsBuilder.addSubtype(
+ typeAndSubtype.second.get(0).substring(1));
}
mMdnsDiscoveryManager.registerListener(
listenServiceType, listener, optionsBuilder.build());
@@ -773,7 +811,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -811,7 +849,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -826,11 +864,15 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
- final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+ final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
+ serviceType);
final String registerServiceType = typeSubtype == null
? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
@@ -842,22 +884,53 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
serviceInfo.setServiceType(registerServiceType);
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
+ Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
+ for (String subType: typeSubtype.second) {
+ if (!TextUtils.isEmpty(subType)) {
+ subtypes.add(subType);
+ }
+ }
+ subtypes = dedupSubtypeLabels(subtypes);
+
+ if (!checkSubtypeLabels(subtypes)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
+ serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
- // TODO: pass in the subtype as well. Including the subtype in the
- // service type would generate service instance names like
- // Name._subtype._sub._type._tcp, which is incorrect
- // (it should be Name._type._tcp).
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+ isUpdateOnly).build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- typeSubtype.second,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -877,7 +950,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -920,7 +993,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -932,7 +1005,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeSubtype =
+ final Pair<String, List<String>> typeSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeSubtype == null
? null : typeSubtype.first;
@@ -982,7 +1055,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1021,7 +1094,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1033,7 +1106,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
+ final Pair<String, List<String>> typeAndSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
@@ -1064,7 +1137,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1388,6 +1461,7 @@
servInfo,
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
+ servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
return servInfo;
}
@@ -1581,34 +1655,36 @@
* underscore; they are alphanumerical characters or dashes or underscore, except the
* last one that is just alphanumerical. The last label must be _tcp or _udp.
*
- * <p>The subtype may also be specified with a comma after the service type, for example
- * _type._tcp,_subtype.
+ * <p>The subtypes may also be specified with a comma after the service type, for example
+ * _type._tcp,_subtype1,_subtype2
*
* @param serviceType the request service type for discovery / resolution service
* @return constructed service type or null if the given service type is invalid.
*/
@Nullable
- public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
+ public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
-
- final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
- final Pattern serviceTypePattern = Pattern.compile(
- // Optional leading subtype (_subtype._type._tcp)
- // (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + typeOrSubtypePattern + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
- // Drop '.' at the end of service type that is compatible with old backend.
- // e.g. allow "_type._tcp.local."
- + "\\.?"
- // Optional subtype after comma, for "_type._tcp,_subtype" format
- + "(?:,(" + typeOrSubtypePattern + "))?"
- + "$");
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
- // Use the subtype either at the beginning or after the comma
- final String subtype = matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
- return new Pair<>(matcher.group(2), subtype);
+ final String queryType = matcher.group(2);
+ // Use the subtype at the beginning
+ if (matcher.group(1) != null) {
+ return new Pair<>(queryType, List.of(matcher.group(1)));
+ }
+ // Use the subtypes at the end
+ final String subTypesStr = matcher.group(3);
+ if (subTypesStr != null && !subTypesStr.isEmpty()) {
+ final String[] subTypes = subTypesStr.substring(1).split(",");
+ return new Pair<>(queryType, List.of(subTypes));
+ }
+
+ return new Pair<>(queryType, Collections.emptyList());
+ }
+
+ /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
+ private static boolean checkSubtypeLabel(String subtype) {
+ return Pattern.compile("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
}
@VisibleForTesting
@@ -1653,6 +1729,8 @@
mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
.setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+ .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2014,20 +2092,33 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2039,8 +2130,8 @@
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2052,8 +2143,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2067,13 +2158,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index fc0e11b..135d957 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -44,6 +44,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
@@ -351,8 +352,7 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
- registration.getSubtype());
+ mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -367,7 +367,8 @@
void updateService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
- mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
+ mAdvertisers.valueAt(i).updateService(
+ id, registration.getServiceInfo().getSubtypes());
}
}
@@ -417,7 +418,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo(), registration.getSubtype());
+ registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -485,16 +486,12 @@
private int mConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
- @Nullable
- private String mSubtype;
-
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
- private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo) {
this.mOriginalName = serviceInfo.getServiceName();
this.mServiceInfo = serviceInfo;
- this.mSubtype = subtype;
}
/**
@@ -507,10 +504,11 @@
}
/**
- * Update subType for the registration.
+ * Update subTypes for the registration.
*/
- public void updateSubtype(@Nullable String subtype) {
- this.mSubtype = subtype;
+ public void updateSubtypes(@NonNull Set<String> subtypes) {
+ mServiceInfo = new NsdServiceInfo(mServiceInfo);
+ mServiceInfo.setSubtypes(subtypes);
}
/**
@@ -540,17 +538,8 @@
// In case of conflict choose a different service name. After the first conflict use
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
- final NsdServiceInfo newInfo = new NsdServiceInfo();
+ final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
newInfo.setServiceName(getUpdatedServiceName(renameCount));
- newInfo.setServiceType(mServiceInfo.getServiceType());
- for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
- newInfo.setAttribute(attr.getKey(),
- attr.getValue() == null ? null : new String(attr.getValue()));
- }
- newInfo.setHost(mServiceInfo.getHost());
- newInfo.setPort(mServiceInfo.getPort());
- newInfo.setNetwork(mServiceInfo.getNetwork());
- // interfaceIndex is not set when registering
return newInfo;
}
@@ -565,11 +554,6 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
-
- @Nullable
- public String getSubtype() {
- return mSubtype;
- }
}
/**
@@ -665,14 +649,14 @@
*
* @param id A unique ID for the service.
* @param service The service info to advertise.
- * @param subtype An optional subtype to advertise the service with.
* @param advertisingOptions The advertising options.
*/
- public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
+ public void addOrUpdateService(int id, NsdServiceInfo service,
MdnsAdvertisingOptions advertisingOptions) {
checkThread();
final Registration existingRegistration = mRegistrations.get(id);
final Network network = service.getNetwork();
+ final Set<String> subtypes = service.getSubtypes();
Registration registration;
if (advertisingOptions.isOnlyUpdate()) {
if (existingRegistration == null) {
@@ -687,10 +671,10 @@
return;
}
- mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
+ mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
registration = existingRegistration;
- registration.updateSubtype(subtype);
+ registration.updateSubtypes(subtypes);
} else {
if (existingRegistration != null) {
mSharedLog.e("Adding duplicate registration for " + service);
@@ -698,9 +682,9 @@
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service, subtype);
+ mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
+ registration = new Registration(service);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@@ -793,15 +777,10 @@
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
- final List<String> subTypes = new ArrayList<>();
- String subType = registration.getSubtype();
- if (subType != null) {
- subTypes.add(subType);
- }
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
- subTypes,
+ new ArrayList<>(nsdServiceInfo.getSubtypes()),
String.join(".", mDeviceHostName),
rawOffloadPacket,
// TODO: define overlayable resources in
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 0a6d8c1..1ad47a3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -41,6 +41,11 @@
*/
public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+ /**
+ * A feature flag to control whether the known-answer suppression should be enabled.
+ */
+ public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -53,17 +58,22 @@
// Flag for label count limit
public final boolean mIsLabelCountLimitEnabled;
+ // Flag for known-answer suppression
+ public final boolean mIsKnownAnswerSuppressionEnabled;
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
- boolean isLabelCountLimitEnabled) {
+ boolean isLabelCountLimitEnabled,
+ boolean isKnownAnswerSuppressionEnabled) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
}
@@ -79,6 +89,7 @@
private boolean mIncludeInetAddressRecordsInProbing;
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
+ private boolean mIsKnownAnswerSuppressionEnabled;
/**
* The constructor for {@link Builder}.
@@ -88,6 +99,7 @@
mIncludeInetAddressRecordsInProbing = false;
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
+ mIsKnownAnswerSuppressionEnabled = false;
}
/**
@@ -132,13 +144,24 @@
}
/**
+ * Set whether the known-answer suppression is enabled.
+ *
+ * @see #NSD_KNOWN_ANSWER_SUPPRESSION
+ */
+ public Builder setIsKnownAnswerSuppressionEnabled(boolean isKnownAnswerSuppressionEnabled) {
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
- mIsLabelCountLimitEnabled);
+ mIsLabelCountLimitEnabled,
+ mIsKnownAnswerSuppressionEnabled);
}
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 463df63..aa40c92 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -37,6 +37,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
+import java.util.Set;
/**
* A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
@@ -232,12 +233,12 @@
* Update an already registered service without sending exit/re-announcement packet.
*
* @param id An exiting service id
- * @param subtype A new subtype
+ * @param subtypes New subtypes
*/
- public void updateService(int id, @Nullable String subtype) {
+ public void updateService(int id, @NonNull Set<String> subtypes) {
// The current implementation is intended to be used in cases where subtypes don't get
// announced.
- mRecordRepository.updateService(id, subtype);
+ mRecordRepository.updateService(id, subtypes);
}
/**
@@ -245,9 +246,8 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
- throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+ public void addService(int id, NsdServiceInfo service) throws NameConflictException {
+ final int replacedExitingService = mRecordRepository.addService(id, service);
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 48ece68..d46a7b5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -74,8 +74,6 @@
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
- // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
- private static final String SUBTYPE_SEPARATOR = "_sub";
// Service type for service enumeration (RFC6763 9.)
private static final String[] DNS_SD_SERVICE_TYPE =
@@ -92,6 +90,7 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ @NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
@@ -161,8 +160,6 @@
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
public final NsdServiceInfo serviceInfo;
- @Nullable
- public final String subtype;
/**
* Whether the service is sending exit announcements and will be destroyed soon.
@@ -185,28 +182,28 @@
private boolean isProbing;
/**
- * Create a ServiceRegistration with only update the subType
+ * Create a ServiceRegistration with only update the subType.
*/
- ServiceRegistration withSubtype(String newSubType) {
- return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
- newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
+ ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+ NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
+ newServiceInfo.setSubtypes(newSubtypes);
+ return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
+ repliedServiceCount, sentPacketCount, exiting, isProbing);
}
-
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
- boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
this.serviceInfo = serviceInfo;
- this.subtype = subtype;
final String[] serviceType = splitServiceType(serviceInfo);
final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
- // Service PTR record
- final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
+ // Service PTR records
+ ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
+ ptrRecords.add(new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
serviceType,
@@ -214,26 +211,17 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */);
-
- if (subtype == null) {
- this.ptrRecords = Collections.singletonList(ptrRecord);
- } else {
- final String[] subtypeName = new String[serviceType.length + 2];
- System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
- subtypeName[0] = subtype;
- subtypeName[1] = SUBTYPE_SEPARATOR;
- final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- subtypeName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */);
-
- this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+ true /* sharedName */));
+ for (String subtype : serviceInfo.getSubtypes()) {
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ MdnsUtils.constructFullSubtype(serviceType, subtype),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ NON_NAME_RECORDS_TTL_MILLIS,
+ serviceName),
+ true /* sharedName */));
}
srvRecord = new RecordInfo<>(
@@ -284,8 +272,8 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
- this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
+ int repliedServiceCount, int sentPacketCount) {
+ this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
false /* exiting */, true /* isProbing */);
}
@@ -328,17 +316,17 @@
* Update a service that already registered in the repository.
*
* @param serviceId An existing service ID.
- * @param subtype A new subtype
+ * @param subtypes New subtypes
* @return
*/
- public void updateService(int serviceId, @Nullable String subtype) {
+ public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
if (existingRegistration == null) {
throw new IllegalArgumentException(
"Service ID must already exist for an update request: " + serviceId);
}
- final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
- subtype);
+ final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+ subtypes);
mServices.put(serviceId, updatedRegistration);
}
@@ -352,8 +340,7 @@
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
- throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -366,7 +353,7 @@
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+ mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
NO_PACKET /* sentPacketCount */);
mServices.put(serviceId, registration);
@@ -516,7 +503,7 @@
// Add answers from general records
addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
- answerInfo, additionalAnswerRecords);
+ answerInfo, additionalAnswerRecords, Collections.emptyList());
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
@@ -524,7 +511,7 @@
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
- answerInfo, additionalAnswerRecords)) {
+ answerInfo, additionalAnswerRecords, packet.answers)) {
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
@@ -577,6 +564,15 @@
return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
+ private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
+ for (MdnsRecord knownAnswer : knownAnswerRecords) {
+ if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Add answers and additional answers for a question, from a ServiceRegistration.
*/
@@ -586,7 +582,8 @@
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
- @NonNull List<MdnsRecord> additionalAnswerRecords) {
+ @NonNull List<MdnsRecord> additionalAnswerRecords,
+ @NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
boolean hasDnsSdSrvRecordAnswer = false;
boolean hasFullyOwnedNameMatch = false;
@@ -615,6 +612,20 @@
}
hasKnownAnswer = true;
+
+ // RFC6762 7.1. Known-Answer Suppression:
+ // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
+ // the answer it would give is already included in the Answer Section
+ // with an RR TTL at least half the correct value. If the RR TTL of the
+ // answer as given in the Answer Section is less than half of the true
+ // RR TTL as known by the Multicast DNS responder, the responder MUST
+ // send an answer so as to update the querier's cache before the record
+ // becomes in danger of expiration.
+ if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ && isKnownAnswer(info.record, knownAnswerRecords)) {
+ continue;
+ }
+
hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
&& CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
@@ -626,8 +637,6 @@
continue;
}
- // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half
-
answerInfo.add(info);
}
@@ -929,7 +938,7 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount);
mServices.put(serviceId, newService);
return makeProbingInfo(
serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index ea3af5e..651b643 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -25,6 +25,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -57,15 +58,46 @@
@NonNull
private final SharedLog mSharedLog;
private final boolean mEnableDebugLog;
+ @NonNull
+ private final Dependencies mDependencies;
+
+ /**
+ * Dependencies of MdnsReplySender, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+ long delayMillis) {
+ handler.sendMessageDelayed(message, delayMillis);
+ }
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what) {
+ handler.removeMessages(what);
+ }
+ }
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
boolean enableDebugLog) {
+ this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog, @NonNull Dependencies dependencies) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
mEnableDebugLog = enableDebugLog;
+ mDependencies = dependencies;
}
/**
@@ -74,7 +106,8 @@
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+ mDependencies.sendMessageDelayed(
+ mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
@@ -104,7 +137,7 @@
*/
public void cancelAll() {
ensureRunningOnHandlerThread(mHandler);
- mHandler.removeMessages(MSG_SEND);
+ mDependencies.removeMessages(mHandler, MSG_SEND);
}
private class SendHandler extends Handler {
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 32f604e..df0a040 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -541,6 +541,9 @@
}
if (response.isComplete()) {
+ // There is a bug here: the newServiceFound is global right now. The state needs
+ // to be per listener because of the responseMatchesOptions() filter.
+ // Otherwise, it won't handle the subType update properly.
if (newServiceFound || serviceBecomesComplete) {
sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 1482ebb..8fc8114 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -233,6 +233,20 @@
&& mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
}
+ /**
+ * Creates a new full subtype name with given service type and subtype labels.
+ *
+ * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
+ * of ["_printer", "_sub", "_http", "_tcp"].
+ */
+ public static String[] constructFullSubtype(String[] serviceType, String subtype) {
+ String[] fullSubtype = new String[serviceType.length + 2];
+ fullSubtype[0] = subtype;
+ fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
+ return fullSubtype;
+ }
+
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
public static class Clock {
/**
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..458d64f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastV()) {
+ // On V+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T and U, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 3da1585..8ee8591 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -22,6 +22,7 @@
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -32,17 +33,20 @@
import android.net.TrafficStats;
import android.os.Binder;
import android.os.DropBoxManager;
+import android.os.SystemClock;
import android.service.NetworkStatsRecorderProto;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.net.module.util.NetworkStatsUtils;
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -79,6 +83,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
private final boolean mWipeOnError;
+ private final boolean mUseFastDataInput;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -89,6 +94,9 @@
private final CombiningRewriter mPendingRewriter;
private WeakReference<NetworkStatsCollection> mComplete;
+ private final NetworkStatsMetricsLogger mMetricsLogger = new NetworkStatsMetricsLogger();
+ @Nullable
+ private final File mStatsDir;
/**
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
@@ -104,11 +112,13 @@
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
mWipeOnError = true;
+ mUseFastDataInput = false;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
mPendingRewriter = null;
+ mStatsDir = null;
}
/**
@@ -116,7 +126,7 @@
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
- boolean wipeOnError) {
+ boolean wipeOnError, boolean useFastDataInput, @Nullable File statsDir) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -125,11 +135,13 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
mWipeOnError = wipeOnError;
+ mUseFastDataInput = useFastDataInput;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
mPendingRewriter = new CombiningRewriter(mPending);
+ mStatsDir = statsDir;
}
public void setPersistThreshold(long thresholdBytes) {
@@ -179,8 +191,16 @@
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
+ final long readStart = SystemClock.elapsedRealtime();
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
mComplete = new WeakReference<NetworkStatsCollection>(res);
+ final long readEnd = SystemClock.elapsedRealtime();
+ // For legacy recorders which are used for data integrity check, which
+ // have wipeOnError flag unset, skip reporting metrics.
+ if (mWipeOnError) {
+ mMetricsLogger.logRecorderFileReading(mCookie, (int) (readEnd - readStart),
+ mStatsDir, res, mUseFastDataInput);
+ }
}
return res;
}
@@ -195,8 +215,12 @@
}
private NetworkStatsCollection loadLocked(long start, long end) {
- if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
- final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+ if (LOGD) {
+ Log.d(TAG, "loadLocked() reading from disk for " + mCookie
+ + " useFastDataInput: " + mUseFastDataInput);
+ }
+ final NetworkStatsCollection res =
+ new NetworkStatsCollection(mBucketDuration, mUseFastDataInput);
try {
mRotator.readMatching(res, start, end);
res.recordCollection(mPending);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 2c9f30c..3ac5e29 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -44,7 +44,6 @@
import static android.net.NetworkStats.TAG_ALL;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
-import static android.net.NetworkStatsCollection.compareStats;
import static android.net.NetworkStatsHistory.FIELD_ALL;
import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_TEST;
@@ -295,6 +294,11 @@
static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
"enable_network_stats_event_logger";
+ static final String NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS =
+ "netstats_fastdatainput_target_attempts";
+ static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
+ static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
+
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager;
@@ -318,6 +322,8 @@
private PersistentInt mImportLegacyAttemptsCounter = null;
private PersistentInt mImportLegacySuccessesCounter = null;
private PersistentInt mImportLegacyFallbacksCounter = null;
+ private PersistentInt mFastDataInputSuccessesCounter = null;
+ private PersistentInt mFastDataInputFallbacksCounter = null;
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
@@ -695,6 +701,24 @@
}
/**
+ * Get the count of using FastDataInput target attempts.
+ */
+ public int getUseFastDataInputTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
+ }
+
+ /**
+ * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+ * string description of difference for debugging purpose.
+ */
+ public String compareStats(@NonNull NetworkStatsCollection a,
+ @NonNull NetworkStatsCollection b, boolean allowKeyChange) {
+ return NetworkStatsCollection.compareStats(a, b, allowKeyChange);
+ }
+
+ /**
* Create a persistent counter for given directory and name.
*/
public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
@@ -892,13 +916,7 @@
synchronized (mStatsLock) {
mSystemReady = true;
- // create data recorders along with historical rotators
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
- mStatsDir, true /* wipeOnError */);
+ makeRecordersLocked();
updatePersistThresholdsLocked();
@@ -963,13 +981,106 @@
private NetworkStatsRecorder buildRecorder(
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
- File baseDir, boolean wipeOnError) {
+ File baseDir, boolean wipeOnError, boolean useFastDataInput) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
- wipeOnError);
+ wipeOnError, useFastDataInput, baseDir);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void makeRecordersLocked() {
+ boolean useFastDataInput = true;
+ try {
+ mFastDataInputSuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME);
+ mFastDataInputFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final int targetAttempts = mDeps.getUseFastDataInputTargetAttempts();
+ int successes = 0;
+ int fallbacks = 0;
+ try {
+ successes = mFastDataInputSuccessesCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the reading was
+ // not successful.
+ fallbacks = mFastDataInputFallbacksCounter.get();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final boolean doComparison;
+ if (useFastDataInput) {
+ // Use FastDataInput if it needs to be evaluated or at least one success.
+ doComparison = targetAttempts > successes + fallbacks;
+ // Set target attempt to -1 as the kill switch to disable the feature.
+ useFastDataInput = targetAttempts >= 0 && (doComparison || successes > 0);
+ } else {
+ // useFastDataInput is false due to previous failures.
+ doComparison = false;
+ }
+
+ // create data recorders along with historical rotators.
+ // Don't wipe on error if comparison is needed.
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir, !doComparison /* wipeOnError */, useFastDataInput);
+
+ if (!doComparison) return;
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder),
+ new MigrationInfo(mUidTagRecorder)
+ };
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */)};
+ boolean success = true;
+ for (int i = 0; i < migrations.length; i++) {
+ try {
+ migrations[i].collection = migrations[i].recorder.getOrLoadCompleteLocked();
+ } catch (Throwable t) {
+ Log.wtf(TAG, "Failed to load collection, skip.", t);
+ success = false;
+ break;
+ }
+ if (!compareImportedToLegacyStats(migrations[i], legacyRecorders[i],
+ false /* allowKeyChange */)) {
+ success = false;
+ break;
+ }
+ }
+
+ try {
+ if (success) {
+ mFastDataInputSuccessesCounter.set(successes + 1);
+ } else {
+ // Fallback.
+ mXtRecorder = legacyRecorders[0];
+ mUidRecorder = legacyRecorders[1];
+ mUidTagRecorder = legacyRecorders[2];
+ mFastDataInputFallbacksCounter.set(fallbacks + 1);
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update counters. success = " + success, e);
+ }
}
@GuardedBy("mStatsLock")
@@ -1068,7 +1179,7 @@
new NetworkStatsSettings.Config(HOUR_IN_MILLIS,
15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig,
- false, mStatsDir, true /* wipeOnError */);
+ false, mStatsDir, true /* wipeOnError */, false /* useFastDataInput */);
final MigrationInfo[] migrations = new MigrationInfo[]{
new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder),
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
@@ -1085,11 +1196,11 @@
legacyRecorders = new NetworkStatsRecorder[]{
null /* dev Recorder */,
buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
- false /* wipeOnError */)};
+ false /* wipeOnError */, false /* useFastDataInput */)};
} else {
legacyRecorders = null;
}
@@ -1120,7 +1231,8 @@
if (runComparison) {
final boolean success =
- compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ compareImportedToLegacyStats(migration, legacyRecorders[i],
+ true /* allowKeyChange */);
if (!success && !dryRunImportOnly) {
tryIncrementLegacyFallbacksCounter();
}
@@ -1243,7 +1355,7 @@
* does not match or throw with exceptions.
*/
private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
- @Nullable NetworkStatsRecorder legacyRecorder) {
+ @Nullable NetworkStatsRecorder legacyRecorder, boolean allowKeyChange) {
final NetworkStatsCollection legacyStats;
// Skip the recorder that doesn't need to be compared.
if (legacyRecorder == null) return true;
@@ -1258,7 +1370,8 @@
// The result of comparison is only for logging.
try {
- final String error = compareStats(migration.collection, legacyStats);
+ final String error = mDeps.compareStats(migration.collection, legacyStats,
+ allowKeyChange);
if (error != null) {
Log.wtf(TAG, "Unexpected comparison result for recorder "
+ legacyRecorder.getCookie() + ": " + error);
@@ -2639,6 +2752,17 @@
}
}
pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
+ pw.print(NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS,
+ mDeps.getUseFastDataInputTargetAttempts());
+ pw.println();
+ try {
+ pw.print("FastDataInput successes", mFastDataInputSuccessesCounter.get());
+ pw.println();
+ pw.print("FastDataInput fallbacks", mFastDataInputFallbacksCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump FastDataInput counters)");
+ }
pw.decreaseIndent();
diff --git a/service/Android.bp b/service/Android.bp
index e2dab9e..7c5da0d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -70,6 +70,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// The library name match the service-connectivity jarjar rules that put the JNI utils in the
@@ -200,7 +203,10 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
visibility: [
"//packages/modules/Connectivity/service-t",
"//packages/modules/Connectivity/tests:__subpackages__",
@@ -225,6 +231,7 @@
],
lint: {
strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -283,12 +290,18 @@
java_library {
name: "service-connectivity-for-tests",
defaults: ["service-connectivity-defaults"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library {
name: "service-connectivity",
defaults: ["service-connectivity-defaults"],
installable: true,
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library_static {
@@ -303,6 +316,9 @@
],
static_libs: ["ConnectivityServiceprotos"],
apex_available: ["com.android.tethering"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..6f9d46f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android V+ and eth\\d+ on
+ Android T and U. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/src/com/android/metrics/NetworkRequestStateInfo.java b/service/src/com/android/metrics/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..e3e172a
--- /dev/null
+++ b/service/src/com/android/metrics/NetworkRequestStateInfo.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.android.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+ private final NetworkRequest mNetworkRequest;
+ private final long mNetworkRequestReceivedTime;
+
+ private enum NetworkRequestState {
+ RECEIVED,
+ REMOVED
+ }
+ private NetworkRequestState mNetworkRequestState;
+ private int mNetworkRequestDurationMillis;
+ private final Dependencies mDependencies;
+
+ NetworkRequestStateInfo(NetworkRequest networkRequest,
+ Dependencies deps) {
+ mDependencies = deps;
+ mNetworkRequest = networkRequest;
+ mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+ mNetworkRequestDurationMillis = 0;
+ mNetworkRequestState = NetworkRequestState.RECEIVED;
+ }
+
+ public void setNetworkRequestRemoved() {
+ mNetworkRequestState = NetworkRequestState.REMOVED;
+ mNetworkRequestDurationMillis = (int) (
+ mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+ }
+
+ public int getNetworkRequestStateStatsType() {
+ if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+ } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+ } else {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+ }
+ }
+
+ public int getRequestId() {
+ return mNetworkRequest.requestId;
+ }
+
+ public int getPackageUid() {
+ return mNetworkRequest.networkCapabilities.getRequestorUid();
+ }
+
+ public int getTransportTypes() {
+ return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+ }
+
+ public boolean getNetCapabilityNotMetered() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ public boolean getNetCapabilityInternet() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+
+ public int getNetworkRequestDurationMillis() {
+ return mNetworkRequestDurationMillis;
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+ // relative to start time and avoid timezone change, including time spent in deep sleep.
+ public long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
diff --git a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java b/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..361ad22
--- /dev/null
+++ b/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,192 @@
+/*
+ * 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 com.android.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+ private static final String TAG = "NetworkRequestStateStatsMetrics";
+ private static final int MSG_NETWORK_REQUEST_STATE_CHANGED = 0;
+
+ // 1 second internal is suggested by experiment team
+ private static final int ATOM_INTERVAL_MS = 1000;
+ private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+ private final Handler mStatsLoggingHandler;
+
+ private final Dependencies mDependencies;
+
+ private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+
+ public NetworkRequestStateStatsMetrics() {
+ this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+ }
+
+ @VisibleForTesting
+ NetworkRequestStateStatsMetrics(Dependencies deps,
+ NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+ mNetworkRequestsActive = new SparseArray<>();
+ mDependencies = deps;
+ mNRStateInfoDeps = nrStateInfoDeps;
+ HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+ handlerThread.start();
+ mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+ }
+
+ /**
+ * Register network request receive event, push RECEIVE atom
+ *
+ * @param networkRequest network request received
+ */
+ public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+ if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+ Log.w(TAG, "Received already registered network request, id = "
+ + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+ + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ networkRequest, mNRStateInfoDeps);
+ mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+ mStatsLoggingHandler.sendMessage(
+ Message.obtain(
+ mStatsLoggingHandler,
+ MSG_NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo));
+ }
+ }
+
+ /**
+ * Register network request remove event, push REMOVE atom
+ *
+ * @param networkRequest network request removed
+ */
+ public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+ NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+ networkRequest.requestId);
+ if (networkRequestStateInfo == null) {
+ Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+
+ mNetworkRequestsActive.remove(networkRequest.requestId);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ mStatsLoggingHandler.sendMessage(
+ Message.obtain(
+ mStatsLoggingHandler,
+ MSG_NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo));
+
+ }
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ /**
+ * Creates a thread with provided tag.
+ *
+ * @param tag for the thread.
+ */
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
+ return new HandlerThread(tag);
+ }
+
+ /**
+ * Sleeps the thread for provided intervalMs millis.
+ *
+ * @param intervalMs number of millis for the thread sleep.
+ */
+ public void threadSleep(int intervalMs) {
+ try {
+ Thread.sleep(intervalMs);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Cool down interrupted!", e);
+ }
+ }
+
+ /**
+ * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+ *
+ * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+ */
+ public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+ ConnectivityStatsLog.write(
+ NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo.getPackageUid(),
+ networkRequestStateInfo.getTransportTypes(),
+ networkRequestStateInfo.getNetCapabilityNotMetered(),
+ networkRequestStateInfo.getNetCapabilityInternet(),
+ networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ }
+ }
+
+ private class StatsLoggingHandler extends Handler {
+ private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+ private long mLastLogTime = 0;
+
+ StatsLoggingHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void checkStatsLoggingTimeout() {
+ // Cool down before next execution. Required by atom logging frequency.
+ long now = SystemClock.elapsedRealtime();
+ if (now - mLastLogTime < ATOM_INTERVAL_MS) {
+ mDependencies.threadSleep(ATOM_INTERVAL_MS);
+ }
+ mLastLogTime = now;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ NetworkRequestStateInfo loggingInfo;
+ switch (msg.what) {
+ case MSG_NETWORK_REQUEST_STATE_CHANGED:
+ checkStatsLoggingTimeout();
+ loggingInfo = (NetworkRequestStateInfo) msg.obj;
+ mDependencies.writeStats(loggingInfo);
+ break;
+ default: // fall out
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 22120e9..b4efa34 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -32,6 +32,7 @@
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
@@ -280,6 +281,7 @@
import com.android.metrics.NetworkDescription;
import com.android.metrics.NetworkList;
import com.android.metrics.NetworkRequestCount;
+import com.android.metrics.NetworkRequestStateStatsMetrics;
import com.android.metrics.RequestCountForType;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.build.SdkLevel;
@@ -940,6 +942,8 @@
private final IpConnectivityLog mMetricsLog;
+ private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -1421,6 +1425,19 @@
}
/**
+ * @see NetworkRequestStateStatsMetrics
+ */
+ public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+ Context context) {
+ // We currently have network requests metric for Watch devices only
+ if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+ return new NetworkRequestStateStatsMetrics();
+ } else {
+ return null;
+ }
+ }
+
+ /**
* @see BatteryStatsManager
*/
public void reportNetworkInterfaceForTransports(Context context, String iface,
@@ -1653,6 +1670,7 @@
new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
mMetricsLog = logger;
+ mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
final NetworkRequest defaultInternetRequest = createDefaultRequest();
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
@@ -1770,6 +1788,19 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
+ // This is needed for pre-V devices to propagate the data saver status
+ // to the BPF map. This isn't supported before Android T because BPF maps are
+ // unsupported, and it's also unnecessary on Android V and later versions,
+ // as the platform code handles data saver bit updates. Additionally, checking
+ // the initial data saver status here is superfluous because the intent won't
+ // be sent until the system is ready.
+ if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+ final IntentFilter dataSaverIntentFilter =
+ new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+ null /* broadcastPermission */, mHandler);
+ }
+
// TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
// But reading the trunk stable flags from mainline modules is not supported yet.
// So enabling this feature on V+ release.
@@ -3503,7 +3534,7 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkSystemBarServicePermission(int pid, int uid) {
+ private boolean checkStatusBarServicePermission(int pid, int uid) {
return checkAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.STATUS_BAR_SERVICE);
}
@@ -5310,6 +5341,8 @@
updateSignalStrengthThresholds(network, "REGISTER", req);
}
}
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
}
}
@@ -5527,6 +5560,8 @@
}
if (req.isListen()) {
removeListenRequestFromNetworks(req);
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
}
}
nri.unlinkDeathRecipient();
@@ -7051,6 +7086,32 @@
}
};
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDeps.isAtLeastV()) {
+ throw new IllegalStateException(
+ "data saver status should be updated from platform");
+ }
+ ensureRunningOnConnectivityServiceThread();
+ switch (intent.getAction()) {
+ case ACTION_RESTRICT_BACKGROUND_CHANGED:
+ // If the uid is present in the deny list, the API will consistently
+ // return ENABLED. To retrieve the global switch status, the system
+ // uid is chosen because it will never be included in the deny list.
+ final int dataSaverForSystemUid =
+ mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+ final boolean isDataSaverEnabled = (dataSaverForSystemUid
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+ mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+ break;
+ default:
+ Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+ }
+ }
+ };
+
private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
@@ -11662,7 +11723,7 @@
return true;
}
if (mAllowSysUiConnectivityReports
- && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ && checkStatusBarServicePermission(callbackPid, callbackUid)) {
return true;
}
@@ -12988,7 +13049,7 @@
}
}
- @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Override
public void setDataSaverEnabled(final boolean enable) {
enforceNetworkStackOrSettingsPermission();
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 8036ae9..94ba9de 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,9 +25,6 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -90,6 +87,7 @@
*/
public class AutomaticOnOffKeepaliveTracker {
private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
@@ -794,22 +792,18 @@
try {
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
- final int startPos = bytes.position();
+ // NetlinkMessage.parse() will move the byte buffer position.
+ // TODO: Parse dst address information to filter socket.
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(
+ bytes, OsConstants.NETLINK_INET_DIAG);
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ if (DBG) Log.e(TAG, "Not a SOCK_DIAG_BY_FAMILY msg");
+ return false;
+ }
- final int nlmsgLen = bytes.getInt();
- final int nlmsgType = bytes.getShort();
- if (isEndOfMessageOrError(nlmsgType)) return false;
- // TODO: Parse InetDiagMessage to get uid and dst address information to filter
- // socket via NetlinkMessage.parse.
-
- // Skip the header to move to data part.
- bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
- if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- bytes.position(startPos);
- final InetDiagMessage diagMsg = (InetDiagMessage) NetlinkMessage.parse(
- bytes, OsConstants.NETLINK_INET_DIAG);
+ final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+ if (isTargetTcpSocket(diagMsg, networkMark, networkMask, vpnUidRanges)) {
+ if (DBG) {
Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
+ " cookie %d",
diagMsg.inetDiagMsg.idiag_uid,
@@ -834,26 +828,31 @@
return false;
}
- private boolean isEndOfMessageOrError(int nlmsgType) {
- return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
+ private static boolean containsUid(Set<Range<Integer>> ranges, int uid) {
+ for (final Range<Integer> range: ranges) {
+ if (range.contains(uid)) {
+ return true;
+ }
+ }
+ return false;
}
- private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
- int networkMask) {
- final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+ private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
+ int networkMark, int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ if (!containsUid(vpnUidRanges, diagMsg.inetDiagMsg.idiag_uid)) return false;
+
+ final int mark = readSocketDataAndReturnMark(diagMsg);
return (mark & networkMask) == networkMark;
}
- private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
- final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+ private int readSocketDataAndReturnMark(@NonNull InetDiagMessage diagMsg) {
int mark = NetlinkUtils.INIT_MARK_VALUE;
// Get socket mark
- // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
- // data.
- while (bytes.position() < nextMsgOffset) {
- final StructNlAttr nlattr = StructNlAttr.parse(bytes);
- if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
- mark = nlattr.getValueAsInteger();
+ for (StructNlAttr attr : diagMsg.nlAttrs) {
+ if (attr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+ // The netlink attributes should contain only one INET_DIAG_MARK for each socket.
+ mark = attr.getValueAsInteger();
+ break;
}
}
return mark;
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index d538221..497b8cb 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -166,6 +166,24 @@
}
/**
+ * Build an ICMPv6 Router Solicitation packet from the required specified parameters without
+ * ethernet header.
+ */
+ public static ByteBuffer buildRsPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+ final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+ final ByteBuffer[] payload =
+ buildIcmpv6Payload(
+ ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+ return buildIcmpv6Packet(
+ srcIp,
+ dstIp,
+ (byte) ICMPV6_ROUTER_SOLICITATION /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /**
* Build an ICMPv6 Echo Request packet from the required specified parameters.
*/
public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
@@ -176,11 +194,21 @@
}
/**
- * Build an ICMPv6 Echo Reply packet without ethernet header.
+ * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+ * header.
*/
- public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
+ public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+ return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /** Build an ICMPv6 Echo Reply packet without ethernet header. */
+ public static ByteBuffer buildEchoReplyPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp) {
+ final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
(byte) 0 /* code */, payload);
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 4f76577..dbd83d0 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -27,6 +27,7 @@
import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
@@ -59,8 +60,11 @@
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/**
@@ -154,7 +158,8 @@
}
public StructInetDiagMsg inetDiagMsg;
-
+ // The netlink attributes.
+ public List<StructNlAttr> nlAttrs = new ArrayList<>();
@VisibleForTesting
public InetDiagMessage(@NonNull StructNlMsgHdr header) {
super(header);
@@ -172,6 +177,16 @@
if (msg.inetDiagMsg == null) {
return null;
}
+ final int payloadLength = header.nlmsg_len - SOCKDIAG_MSG_HEADER_SIZE;
+ final ByteBuffer payload = byteBuffer.slice();
+ while (payload.position() < payloadLength) {
+ final StructNlAttr attr = StructNlAttr.parse(payload);
+ // Stop parsing for truncated or malformed attribute
+ if (attr == null) return null;
+
+ msg.nlAttrs.add(attr);
+ }
+
return msg;
}
@@ -307,9 +322,8 @@
NetlinkUtils.receiveNetlinkAck(fd);
}
- private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
- throws InterruptedIOException, ErrnoException {
- final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+ private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+ return InetDiagMessage.inetDiagReqV2(
proto,
null /* id */,
family,
@@ -318,51 +332,29 @@
0 /* pad */,
0 /* idiagExt */,
states);
- NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
}
- private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+ private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
- throws InterruptedIOException, ErrnoException {
- int destroyedSockets = 0;
-
- while (true) {
- final ByteBuffer buf = NetlinkUtils.recvMessage(
- dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-
- while (buf.remaining() > 0) {
- final int position = buf.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
- if (nlMsg == null) {
- // Move to the position where parse started for error log.
- buf.position(position);
- Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
- break;
- }
-
- if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
- return destroyedSockets;
- }
-
- if (!(nlMsg instanceof InetDiagMessage)) {
- Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
- continue;
- }
-
- final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
- if (filter.test(diagMsg)) {
- try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
- destroyedSockets++;
- } catch (InterruptedIOException | ErrnoException e) {
- if (!(e instanceof ErrnoException
- && ((ErrnoException) e).errno == ENOENT)) {
- Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
- }
+ throws SocketException, InterruptedIOException, ErrnoException {
+ AtomicInteger destroyedSockets = new AtomicInteger(0);
+ Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
+ if (filter.test(diagMsg)) {
+ try {
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ destroyedSockets.getAndIncrement();
+ } catch (InterruptedIOException | ErrnoException e) {
+ if (!(e instanceof ErrnoException
+ && ((ErrnoException) e).errno == ENOENT)) {
+ Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
}
}
}
- }
+ };
+
+ NetlinkUtils.<InetDiagMessage>getAndProcessNetlinkDumpMessages(dumpReq,
+ NETLINK_INET_DIAG, InetDiagMessage.class, handleNlDumpMsg);
+ return destroyedSockets.get();
}
/**
@@ -420,31 +412,28 @@
private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
throws ErrnoException, SocketException, InterruptedIOException {
- FileDescriptor dumpFd = null;
FileDescriptor destroyFd = null;
try {
- dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
- connectToKernel(dumpFd);
connectToKernel(destroyFd);
for (int family : List.of(AF_INET, AF_INET6)) {
+ byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
try {
- sendNetlinkDumpRequest(dumpFd, proto, states, family);
- } catch (InterruptedIOException | ErrnoException e) {
- Log.e(TAG, "Failed to send netlink dump request: " + e);
- continue;
- }
- final int destroyedSockets = processNetlinkDumpAndDestroySockets(
- dumpFd, destroyFd, proto, filter);
- Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+ req, destroyFd, proto, filter);
+ Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ ", proto=" + stringForProtocol(proto)
+ ", family=" + stringForAddressFamily(family)
+ ", states=" + states);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send netlink dump request or receive messages: " + e);
+ continue;
+ }
}
} finally {
- closeSocketQuietly(dumpFd);
closeSocketQuietly(destroyFd);
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 44c51d8..ad7a4d7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -151,6 +151,9 @@
public static final int RTNLGRP_ND_USEROPT = 20;
public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
+ // Netlink family
+ public static final short RTNL_FAMILY_IP6MR = 129;
+
// Device flags.
public static final int IFF_UP = 1 << 0;
public static final int IFF_LOWER_UP = 1 << 16;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 111e0ba..781a04e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -142,6 +142,7 @@
return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWROUTE:
case NetlinkConstants.RTM_DELROUTE:
+ case NetlinkConstants.RTM_GETROUTE:
return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWNEIGH:
case NetlinkConstants.RTM_DELNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..7c2be2c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -29,6 +29,11 @@
import static android.system.OsConstants.SO_RCVBUF;
import static android.system.OsConstants.SO_RCVTIMEO;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
import android.net.util.SocketUtils;
import android.system.ErrnoException;
@@ -47,7 +52,11 @@
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* Utilities for netlink related class that may not be able to fit into a specific class.
@@ -163,11 +172,7 @@
Log.e(TAG, errPrefix, e);
throw new ErrnoException(errPrefix, EIO, e);
} finally {
- try {
- SocketUtils.closeSocket(fd);
- } catch (IOException e) {
- // Nothing we can do here
- }
+ closeSocketQuietly(fd);
}
}
@@ -308,4 +313,139 @@
}
private NetlinkUtils() {}
+
+ private static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessagesWithFd(
+ FileDescriptor fd, byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // connecToKernel throws ErrnoException and SocketException, should be handled by caller
+ connectToKernel(fd);
+
+ // sendMessage throws InterruptedIOException and ErrnoException,
+ // should be handled by caller
+ sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+
+ while (true) {
+ // recvMessage throws ErrnoException, InterruptedIOException
+ // should be handled by caller
+ final ByteBuffer buf = recvMessage(
+ fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+ while (buf.remaining() > 0) {
+ final int position = buf.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, nlFamily);
+ if (nlMsg == null) {
+ // Move to the position where parse started for error log.
+ buf.position(position);
+ Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+ break;
+ }
+
+ if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+ return;
+ }
+
+ if (!msgClass.isInstance(nlMsg)) {
+ Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+ continue;
+ }
+
+ final T msg = (T) nlMsg;
+ func.accept(msg);
+ }
+ }
+ }
+ /**
+ * Sends a netlink dump request and processes the returned dump messages
+ *
+ * @param <T> extends NetlinkMessage
+ * @param dumpRequestMessage netlink dump request message to be sent
+ * @param nlFamily netlink family
+ * @param msgClass expected class of the netlink message
+ * @param func function defined by caller to handle the dump messages
+ * @throws SocketException when fails to connect socket to kernel
+ * @throws InterruptedIOException when fails to read the dumpFd
+ * @throws ErrnoException when fails to create dump fd, send dump request
+ * or receive messages
+ */
+ public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+ byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // Create socket
+ final FileDescriptor fd = netlinkSocketForProto(nlFamily);
+ try {
+ getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
+ msgClass, func);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
+ /**
+ * Construct a RTM_GETROUTE message for dumping multicast IPv6 routes from kernel.
+ */
+ private static byte[] newIpv6MulticastRouteDumpRequest() {
+ final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+ nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETROUTE;
+ nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ final short shortZero = 0;
+
+ // family must be RTNL_FAMILY_IP6MR to dump IPv6 multicast routes.
+ // dstLen, srcLen, tos and scope must be zero in FIB dump request.
+ // protocol, flags must be 0, and type must be RTN_MULTICAST (if not 0) for multicast
+ // dump request.
+ // table or RTA_TABLE attributes can be used to dump a specific routing table.
+ // RTA_OIF attribute can be used to dump only routes containing given oif.
+ // Here no attributes are set so the kernel can return all multicast routes.
+ final StructRtMsg rtMsg =
+ new StructRtMsg(RTNL_FAMILY_IP6MR /* family */, shortZero /* dstLen */,
+ shortZero /* srcLen */, shortZero /* tos */, shortZero /* table */,
+ shortZero /* protocol */, shortZero /* scope */, shortZero /* type */,
+ 0L /* flags */);
+ final RtNetlinkRouteMessage msg =
+ new RtNetlinkRouteMessage(nlmsghdr, rtMsg);
+
+ final int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructRtMsg.STRUCT_SIZE;
+ nlmsghdr.nlmsg_len = spaceRequired;
+ final byte[] bytes = new byte[NetlinkConstants.alignedLengthOf(spaceRequired)];
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ msg.pack(byteBuffer);
+ return bytes;
+ }
+
+ /**
+ * Get the list of IPv6 multicast route messages from kernel.
+ */
+ public static List<RtNetlinkRouteMessage> getIpv6MulticastRoutes() {
+ final byte[] dumpMsg = newIpv6MulticastRouteDumpRequest();
+ List<RtNetlinkRouteMessage> routes = new ArrayList<>();
+ Consumer<RtNetlinkRouteMessage> handleNlDumpMsg = (msg) -> {
+ if (msg.getRtmFamily() == RTNL_FAMILY_IP6MR) {
+ // Sent rtmFamily RTNL_FAMILY_IP6MR in dump request to make sure ipv6
+ // multicast routes are included in netlink reply messages, the kernel
+ // may also reply with other kind of routes, so we filter them out here.
+ routes.add(msg);
+ }
+ };
+ try {
+ NetlinkUtils.<RtNetlinkRouteMessage>getAndProcessNetlinkDumpMessages(
+ dumpMsg, NETLINK_ROUTE, RtNetlinkRouteMessage.class,
+ handleNlDumpMsg);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to dump multicast routes");
+ return routes;
+ }
+
+ return routes;
+ }
+
+ private static void closeSocketQuietly(final FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException e) {
+ // Nothing we can do here
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index 9acac69..b2b1e93 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -19,11 +19,15 @@
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.NETLINK_ROUTE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import android.annotation.SuppressLint;
import android.net.IpPrefix;
+import android.net.RouteInfo;
import android.system.OsConstants;
import androidx.annotation.NonNull;
@@ -34,6 +38,9 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.util.Arrays;
/**
* A NetlinkMessage subclass for rtnetlink route messages.
@@ -49,31 +56,69 @@
*/
public class RtNetlinkRouteMessage extends NetlinkMessage {
public static final short RTA_DST = 1;
+ public static final short RTA_SRC = 2;
+ public static final short RTA_IIF = 3;
public static final short RTA_OIF = 4;
public static final short RTA_GATEWAY = 5;
public static final short RTA_CACHEINFO = 12;
+ public static final short RTA_EXPIRES = 23;
- private int mIfindex;
+ public static final short RTNH_F_UNRESOLVED = 32; // The multicast route is unresolved
+
+ public static final String TAG = "NetlinkRouteMessage";
+
+ // For multicast routes, whether the route is resolved or unresolved
+ private boolean mIsResolved;
+ // The interface index for incoming interface, this is set for multicast
+ // routes, see common/net/ipv4/ipmr_base.c mr_fill_mroute
+ private int mIifIndex; // Incoming interface of a route, for resolved multicast routes
+ private int mOifIndex;
@NonNull
private StructRtMsg mRtmsg;
- @NonNull
- private IpPrefix mDestination;
+ @Nullable
+ private IpPrefix mSource; // Source address of a route, for all multicast routes
+ @Nullable
+ private IpPrefix mDestination; // Destination of a route, can be null for RTM_GETROUTE
@Nullable
private InetAddress mGateway;
@Nullable
private StructRtaCacheInfo mRtaCacheInfo;
+ private long mSinceLastUseMillis; // Milliseconds since the route was used,
+ // for resolved multicast routes
- private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+ public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
super(header);
- mRtmsg = null;
+ mRtmsg = rtMsg;
+ mSource = null;
mDestination = null;
mGateway = null;
- mIfindex = 0;
+ mIifIndex = 0;
+ mOifIndex = 0;
mRtaCacheInfo = null;
+ mSinceLastUseMillis = -1;
+ }
+
+ /**
+ * Returns the rtnetlink family.
+ */
+ public short getRtmFamily() {
+ return mRtmsg.family;
+ }
+
+ /**
+ * Returns if the route is resolved. This is always true for unicast,
+ * and may be false only for multicast routes.
+ */
+ public boolean isResolved() {
+ return mIsResolved;
+ }
+
+ public int getIifIndex() {
+ return mIifIndex;
}
public int getInterfaceIndex() {
- return mIfindex;
+ return mOifIndex;
}
@NonNull
@@ -86,6 +131,14 @@
return mDestination;
}
+ /**
+ * Get source address of a route. This is for multicast routes.
+ */
+ @NonNull
+ public IpPrefix getSource() {
+ return mSource;
+ }
+
@Nullable
public InetAddress getGateway() {
return mGateway;
@@ -97,6 +150,18 @@
}
/**
+ * RTA_EXPIRES attribute returned by kernel to indicate the clock ticks
+ * from the route was last used to now, converted to milliseconds.
+ * This is set for multicast routes.
+ *
+ * Note that this value is not updated with the passage of time. It always
+ * returns the value that was read when the netlink message was parsed.
+ */
+ public long getSinceLastUseMillis() {
+ return mSinceLastUseMillis;
+ }
+
+ /**
* Check whether the address families of destination and gateway match rtm_family in
* StructRtmsg.
*
@@ -107,7 +172,8 @@
private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
int family) {
return ((address instanceof Inet4Address) && (family == AF_INET))
- || ((address instanceof Inet6Address) && (family == AF_INET6));
+ || ((address instanceof Inet6Address) &&
+ (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
}
/**
@@ -121,11 +187,11 @@
@Nullable
public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
@NonNull final ByteBuffer byteBuffer) {
- final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
-
- routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
- if (routeMsg.mRtmsg == null) return null;
+ final StructRtMsg rtmsg = StructRtMsg.parse(byteBuffer);
+ if (rtmsg == null) return null;
+ final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header, rtmsg);
int rtmFamily = routeMsg.mRtmsg.family;
+ routeMsg.mIsResolved = ((routeMsg.mRtmsg.flags & RTNH_F_UNRESOLVED) == 0);
// RTA_DST
final int baseOffset = byteBuffer.position();
@@ -139,12 +205,24 @@
routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
} else if (rtmFamily == AF_INET) {
routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
- } else if (rtmFamily == AF_INET6) {
+ } else if (rtmFamily == AF_INET6 || rtmFamily == RTNL_FAMILY_IP6MR) {
routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
} else {
return null;
}
+ // RTA_SRC
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_SRC, byteBuffer);
+ if (nlAttr != null) {
+ final InetAddress source = nlAttr.getValueAsInetAddress();
+ // If the RTA_SRC attribute is malformed, return null.
+ if (source == null) return null;
+ // If the address family of destination doesn't match rtm_family, return null.
+ if (!matchRouteAddressFamily(source, rtmFamily)) return null;
+ routeMsg.mSource = new IpPrefix(source, routeMsg.mRtmsg.srcLen);
+ }
+
// RTA_GATEWAY
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
@@ -156,6 +234,17 @@
if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
}
+ // RTA_IIF
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_IIF, byteBuffer);
+ if (nlAttr != null) {
+ Integer iifInteger = nlAttr.getValueAsInteger();
+ if (iifInteger == null) {
+ return null;
+ }
+ routeMsg.mIifIndex = iifInteger;
+ }
+
// RTA_OIF
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
@@ -164,7 +253,7 @@
// the interface index to a name themselves. This may not succeed or may be
// incorrect, because the interface might have been deleted, or even deleted
// and re-added with a different index, since the netlink message was sent.
- routeMsg.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+ routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
}
// RTA_CACHEINFO
@@ -174,33 +263,59 @@
routeMsg.mRtaCacheInfo = StructRtaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
}
+ // RTA_EXPIRES
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_EXPIRES, byteBuffer);
+ if (nlAttr != null) {
+ final Long sinceLastUseCentis = nlAttr.getValueAsLong();
+ // If the RTA_EXPIRES attribute is malformed, return null.
+ if (sinceLastUseCentis == null) return null;
+ // RTA_EXPIRES returns time in clock ticks of USER_HZ(100), which is centiseconds
+ routeMsg.mSinceLastUseMillis = sinceLastUseCentis * 10;
+ }
+
return routeMsg;
}
/**
* Write a rtnetlink address message to {@link ByteBuffer}.
*/
- @VisibleForTesting
- protected void pack(ByteBuffer byteBuffer) {
+ public void pack(ByteBuffer byteBuffer) {
getHeader().pack(byteBuffer);
mRtmsg.pack(byteBuffer);
- final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
- destination.pack(byteBuffer);
+ if (mSource != null) {
+ final StructNlAttr source = new StructNlAttr(RTA_SRC, mSource.getAddress());
+ source.pack(byteBuffer);
+ }
+
+ if (mDestination != null) {
+ final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+ destination.pack(byteBuffer);
+ }
if (mGateway != null) {
final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
gateway.pack(byteBuffer);
}
- if (mIfindex != 0) {
- final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
- ifindex.pack(byteBuffer);
+ if (mIifIndex != 0) {
+ final StructNlAttr iifindex = new StructNlAttr(RTA_IIF, mIifIndex);
+ iifindex.pack(byteBuffer);
+ }
+ if (mOifIndex != 0) {
+ final StructNlAttr oifindex = new StructNlAttr(RTA_OIF, mOifIndex);
+ oifindex.pack(byteBuffer);
}
if (mRtaCacheInfo != null) {
final StructNlAttr cacheInfo = new StructNlAttr(RTA_CACHEINFO,
mRtaCacheInfo.writeToBytes());
cacheInfo.pack(byteBuffer);
}
+ if (mSinceLastUseMillis >= 0) {
+ final long sinceLastUseCentis = mSinceLastUseMillis / 10;
+ final StructNlAttr expires = new StructNlAttr(RTA_EXPIRES, sinceLastUseCentis);
+ expires.pack(byteBuffer);
+ }
}
@Override
@@ -208,10 +323,14 @@
return "RtNetlinkRouteMessage{ "
+ "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ "Rtmsg{" + mRtmsg.toString() + "}, "
- + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+ + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+ + (mDestination == null ?
+ "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
+ "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
- + "ifindex{" + mIfindex + "}, "
+ + (mIifIndex == 0 ? "" : "iifindex{" + mIifIndex + "}, ")
+ + "oifindex{" + mOifIndex + "}, "
+ "rta_cacheinfo{" + (mRtaCacheInfo == null ? "" : mRtaCacheInfo.toString()) + "} "
+ + (mSinceLastUseMillis < 0 ? "" : "sinceLastUseMillis{" + mSinceLastUseMillis + "}")
+ "}";
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index a9b6495..43e8312 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import static java.nio.ByteOrder.nativeOrder;
+
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -152,12 +154,12 @@
nla_type = type;
setValue(new byte[Short.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putShort(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
}
}
@@ -169,12 +171,29 @@
nla_type = type;
setValue(new byte[Integer.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putInt(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
+ }
+ }
+
+ public StructNlAttr(short type, long value) {
+ this(type, value, ByteOrder.nativeOrder());
+ }
+
+ public StructNlAttr(short type, long value, ByteOrder order) {
+ nla_type = type;
+ setValue(new byte[Long.BYTES]);
+ final ByteBuffer buf = getValueAsByteBuffer();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+ try {
+ buf.order(order);
+ buf.putLong(value);
+ } finally {
+ buf.order(nativeOrder());
}
}
@@ -288,6 +307,7 @@
/**
* Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
+ * The attribute value is assumed to be in native byte order.
*/
public Integer getValueAsInteger() {
final ByteBuffer byteBuffer = getValueAsByteBuffer();
@@ -298,6 +318,18 @@
}
/**
+ * Get attribute value as Long, or null if malformed (e.g., length is not 8 bytes).
+ * The attribute value is assumed to be in native byte order.
+ */
+ public Long getValueAsLong() {
+ final ByteBuffer byteBuffer = getValueAsByteBuffer();
+ if (byteBuffer == null || byteBuffer.remaining() != Long.BYTES) {
+ return null;
+ }
+ return byteBuffer.getLong();
+ }
+
+ /**
* Get attribute value as Int, default value if malformed.
*/
public int getValueAsInt(int defaultValue) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index ff37639..5272366 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -71,13 +71,17 @@
}
sb.append("NLM_F_ECHO");
}
- if ((flags & NLM_F_ROOT) != 0) {
+ if ((flags & NLM_F_DUMP) == NLM_F_DUMP) {
+ if (sb.length() > 0) {
+ sb.append("|");
+ }
+ sb.append("NLM_F_DUMP");
+ } else if ((flags & NLM_F_ROOT) != 0) { // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
if (sb.length() > 0) {
sb.append("|");
}
sb.append("NLM_F_ROOT");
- }
- if ((flags & NLM_F_MATCH) != 0) {
+ } else if ((flags & NLM_F_MATCH) != 0) {
if (sb.length() > 0) {
sb.append("|");
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
index 01dc66e..a7bdcd9 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
@@ -131,12 +131,12 @@
/** Return the TX sequence number in unisgned long */
public long getTxSequenceNumber() {
- return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
+ return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
}
/** Return the RX sequence number in unisgned long */
public long getRxSequenceNumber() {
- return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
+ return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
}
@VisibleForTesting
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
new file mode 100644
index 0000000..24e0a97
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.util.Set;
+
+/*
+ * Implements the mf6cctl structure which is used to add a multicast forwarding
+ * cache, see /usr/include/linux/mroute6.h
+ */
+public class StructMf6cctl extends Struct {
+ // struct sockaddr_in6 mf6cc_origin, added the fields directly as Struct
+ // doesn't support nested Structs
+ @Field(order = 0, type = Type.U16)
+ public final int originFamily; // AF_INET6
+ @Field(order = 1, type = Type.U16)
+ public final int originPort; // Transport layer port # of origin
+ @Field(order = 2, type = Type.U32)
+ public final long originFlowinfo; // IPv6 flow information
+ @Field(order = 3, type = Type.ByteArray, arraysize = 16)
+ public final byte[] originAddress; //the IPv6 address of origin
+ @Field(order = 4, type = Type.U32)
+ public final long originScopeId; // scope id, not used
+
+ // struct sockaddr_in6 mf6cc_mcastgrp
+ @Field(order = 5, type = Type.U16)
+ public final int groupFamily; // AF_INET6
+ @Field(order = 6, type = Type.U16)
+ public final int groupPort; // Transport layer port # of multicast group
+ @Field(order = 7, type = Type.U32)
+ public final long groupFlowinfo; // IPv6 flow information
+ @Field(order = 8, type = Type.ByteArray, arraysize = 16)
+ public final byte[] groupAddress; //the IPv6 address of multicast group
+ @Field(order = 9, type = Type.U32)
+ public final long groupScopeId; // scope id, not used
+
+ @Field(order = 10, type = Type.U16, padding = 2)
+ public final int mf6ccParent; // incoming interface
+ @Field(order = 11, type = Type.ByteArray, arraysize = 32)
+ public final byte[] mf6ccIfset; // outgoing interfaces
+
+ public StructMf6cctl(final Inet6Address origin, final Inet6Address group,
+ final int mf6ccParent, final Set<Integer> oifset) {
+ this(AF_INET6, 0, (long) 0, origin.getAddress(), (long) 0, AF_INET6,
+ 0, (long) 0, group.getAddress(), (long) 0, mf6ccParent,
+ getMf6ccIfsetBytes(oifset));
+ }
+
+ private StructMf6cctl(int originFamily, int originPort, long originFlowinfo,
+ byte[] originAddress, long originScopeId, int groupFamily, int groupPort,
+ long groupFlowinfo, byte[] groupAddress, long groupScopeId, int mf6ccParent,
+ byte[] mf6ccIfset) {
+ this.originFamily = originFamily;
+ this.originPort = originPort;
+ this.originFlowinfo = originFlowinfo;
+ this.originAddress = originAddress;
+ this.originScopeId = originScopeId;
+ this.groupFamily = groupFamily;
+ this.groupPort = groupPort;
+ this.groupFlowinfo = groupFlowinfo;
+ this.groupAddress = groupAddress;
+ this.groupScopeId = groupScopeId;
+ this.mf6ccParent = mf6ccParent;
+ this.mf6ccIfset = mf6ccIfset;
+ }
+
+ private static byte[] getMf6ccIfsetBytes(final Set<Integer> oifs)
+ throws IllegalArgumentException {
+ byte[] mf6ccIfset = new byte[32];
+ for (int oif : oifs) {
+ int idx = oif / 8;
+ if (idx >= 32) {
+ // invalid oif index, too big to fit in mf6ccIfset
+ throw new IllegalArgumentException("Invalid oif index" + oif);
+ }
+ int offset = oif % 8;
+ mf6ccIfset[idx] |= (byte) (1 << offset);
+ }
+ return mf6ccIfset;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
new file mode 100644
index 0000000..626a170
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
@@ -0,0 +1,46 @@
+/*
+ * 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 com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+
+/*
+ * Implements the mif6ctl structure which is used to add a multicast routing
+ * interface, see /usr/include/linux/mroute6.h
+ */
+public class StructMif6ctl extends Struct {
+ @Field(order = 0, type = Type.U16)
+ public final int mif6cMifi; // Index of MIF
+ @Field(order = 1, type = Type.U8)
+ public final short mif6cFlags; // MIFF_ flags
+ @Field(order = 2, type = Type.U8)
+ public final short vifcThreshold; // ttl limit
+ @Field(order = 3, type = Type.U16)
+ public final int mif6cPifi; //the index of the physical IF
+ @Field(order = 4, type = Type.U32, padding = 2)
+ public final long vifcRateLimit; // Rate limiter values (NI)
+
+ public StructMif6ctl(final int mif6cMifi, final short mif6cFlags, final short vifcThreshold,
+ final int mif6cPifi, final long vifcRateLimit) {
+ this.mif6cMifi = mif6cMifi;
+ this.mif6cFlags = mif6cFlags;
+ this.vifcThreshold = vifcThreshold;
+ this.mif6cPifi = mif6cPifi;
+ this.vifcRateLimit = vifcRateLimit;
+ }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
new file mode 100644
index 0000000..569e361
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
@@ -0,0 +1,52 @@
+/*
+ * 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 com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class StructMrt6Msg extends Struct {
+ public static final byte MRT6MSG_NOCACHE = 1;
+
+ @Field(order = 0, type = Type.S8)
+ public final byte mbz;
+ @Field(order = 1, type = Type.S8)
+ public final byte msgType; // message type
+ @Field(order = 2, type = Type.U16, padding = 4)
+ public final int mif; // mif received on
+ @Field(order = 3, type = Type.Ipv6Address)
+ public final Inet6Address src;
+ @Field(order = 4, type = Type.Ipv6Address)
+ public final Inet6Address dst;
+
+ public StructMrt6Msg(final byte mbz, final byte msgType, final int mif,
+ final Inet6Address source, final Inet6Address destination) {
+ this.mbz = mbz; // kernel should set it to 0
+ this.msgType = msgType;
+ this.mif = mif;
+ this.src = source;
+ this.dst = destination;
+ }
+
+ public static StructMrt6Msg parse(ByteBuffer byteBuffer) {
+ byteBuffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(StructMrt6Msg.class, byteBuffer);
+ }
+}
+
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 637a938..2b7e620 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -241,5 +241,17 @@
min_sdk_version: "30",
},
},
- versions: ["1"],
+ versions_with_info: [
+ {
+ version: "1",
+ imports: [],
+ },
+ {
+ version: "2",
+ imports: [],
+ },
+
+ ],
+ frozen: true,
+
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
new file mode 100644
index 0000000..785d42d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
@@ -0,0 +1 @@
+0e5d9ad0664b8b3ec9d323534c42333cf6f6ed3d
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domainName;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+ int id;
+ int result;
+ @utf8InCpp String hostname;
+ @utf8InCpp String address;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+ const int SERVICE_DISCOVERY_FAILED = 602;
+ const int SERVICE_FOUND = 603;
+ const int SERVICE_LOST = 604;
+ const int SERVICE_REGISTRATION_FAILED = 605;
+ const int SERVICE_REGISTERED = 606;
+ const int SERVICE_RESOLUTION_FAILED = 607;
+ const int SERVICE_RESOLVED = 608;
+ const int SERVICE_GET_ADDR_FAILED = 611;
+ const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domain;
+ @utf8InCpp String serviceFullName;
+ @utf8InCpp String hostname;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
index ecbe966..d84742b 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
@@ -34,13 +34,40 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
index 4625cac..187a3d2 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -34,9 +34,21 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
index 255d70f..3bf1da8 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -28,6 +28,8 @@
* Start the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EALREADY if daemon is already running.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void startDaemon();
@@ -35,6 +37,8 @@
* Stop the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EBUSY if daemon is still in use.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void stopDaemon();
@@ -49,6 +53,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if registration fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerService(in RegistrationInfo info);
@@ -63,6 +69,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if discovery fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void discover(in DiscoveryInfo info);
@@ -77,6 +85,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if resolution fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void resolve(in ResolutionInfo info);
@@ -92,6 +102,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if getting address fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void getServiceAddress(in GetAddressInfo info);
@@ -101,6 +113,8 @@
* @param id the operation id to be stopped.
*
* @throws ServiceSpecificException with unix errno ESRCH if request id is not in use.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void stopOperation(int id);
@@ -112,6 +126,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EINVAL if listener is null.
* - Unix errno EEXIST if register duplicated listener.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerEventListener(in IMDnsEventListener listener);
@@ -121,6 +137,8 @@
* @param listener The listener to be unregistered.
*
* @throws ServiceSpecificException with unix errno EINVAL if listener is null.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void unregisterEventListener(in IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
index a202a26..f7f028b 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -31,8 +31,8 @@
oneway interface IMDnsEventListener {
/**
* Types for MDNS operation result.
- * These are in sync with frameworks/libs/net/common/netd/libnetdutils/include/netdutils/\
- * ResponseCode.h
+ * These are in sync with packages/modules/Connectivity/staticlibs/netd/libnetdutils/include/\
+ * netdutils/ResponseCode.h
*/
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
@@ -46,21 +46,29 @@
/**
* Notify service registration status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceRegistrationStatus(in RegistrationInfo status);
/**
* Notify service discovery status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceDiscoveryStatus(in DiscoveryInfo status);
/**
* Notify service resolution status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceResolutionStatus(in ResolutionInfo status);
/**
* Notify getting service address status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onGettingServiceAddressStatus(in GetAddressInfo status);
}
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 031e52f..0dfca57 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -33,7 +33,10 @@
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/NetworkStack/tests/integration",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ test: true
+ },
}
android_test {
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index 65e99f8..b44e428 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -32,6 +32,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -345,28 +346,28 @@
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX1 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "58000000" // length = 88
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "01" + // idiag_state = 1
- "02" + // idiag_timer = 2
- "ff" + // idiag_retrans = 255
+ + "0a" // family = AF_INET6
+ + "01" // idiag_state = 1
+ + "02" // idiag_timer = 2
+ + "ff" // idiag_retrans = 255
// inet_diag_sockid
- "a817" + // idiag_sport = 43031
- "960f" + // idiag_dport = 38415
- "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
- "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
- "07000000" + // idiag_if = 7
- "5800000000000000" + // idiag_cookie = 88
- "04000000" + // idiag_expires = 4
- "05000000" + // idiag_rqueue = 5
- "06000000" + // idiag_wqueue = 6
- "a3270000" + // idiag_uid = 10147
- "a57e19f0"; // idiag_inode = 4028202661
+ + "a817" // idiag_sport = 43031
+ + "960f" // idiag_dport = 38415
+ + "20010db8000000000000000000000001" // idiag_src = 2001:db8::1
+ + "20010db8000000000000000000000002" // idiag_dst = 2001:db8::2
+ + "07000000" // idiag_if = 7
+ + "5800000000000000" // idiag_cookie = 88
+ + "04000000" // idiag_expires = 4
+ + "05000000" // idiag_rqueue = 5
+ + "06000000" // idiag_wqueue = 6
+ + "a3270000" // idiag_uid = 10147
+ + "a57e19f0"; // idiag_inode = 4028202661
private void assertInetDiagMsg1(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -394,33 +395,45 @@
assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the length of attribute list is 0 as expected since message doesn't
+ // take any attributes
+ assertEquals(0, inetDiagMsg.nlAttrs.size());
}
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX2 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "6C000000" // length = 108
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "02" + // idiag_state = 2
- "10" + // idiag_timer = 16
- "20" + // idiag_retrans = 32
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
// inet_diag_sockid
- "a845" + // idiag_sport = 43077
- "01bb" + // idiag_dport = 443
- "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
- "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
- "08000000" + // idiag_if = 8
- "6300000000000000" + // idiag_cookie = 99
- "30000000" + // idiag_expires = 48
- "40000000" + // idiag_rqueue = 64
- "50000000" + // idiag_wqueue = 80
- "39300000" + // idiag_uid = 12345
- "851a0000"; // idiag_inode = 6789
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000003" // idiag_src = 2001:db8::3
+ + "20010db8000000000000000000000004" // idiag_dst = 2001:db8::4
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200"; // type = 2
private void assertInetDiagMsg2(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -448,6 +461,104 @@
assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the number of nlAttr and their content.
+ assertEquals(3, inetDiagMsg.nlAttrs.size());
+
+ assertEquals(5, inetDiagMsg.nlAttrs.get(0).nla_len);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(0).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(0).nla_value);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(1).nla_len);
+ assertEquals(15, inetDiagMsg.nlAttrs.get(1).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("850A0C00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(1).nla_value);
+ assertEquals(4, inetDiagMsg.nlAttrs.get(2).nla_len);
+ assertEquals(2, inetDiagMsg.nlAttrs.get(2).nla_type);
+ assertNull(inetDiagMsg.nlAttrs.get(2).nla_value);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_MALFORMED =
+ // struct nlmsghdr
+ "6E000000" // length = 110
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200" // type = 2
+ + "0100" // len = 1, malformed value
+ + "0100"; // type = 1
+
+ @Test
+ public void testParseInetDiagResponseMalformedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_MALFORMED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_TRUNCATED =
+ // struct nlmsghdr
+ "5E000000" // length = 96
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0800" // len = 8
+ + "0100" // type = 1
+ + "000000"; // data, less than the expected length
+
+ @Test
+ public void testParseInetDiagResponseTruncatedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_TRUNCATED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
}
private static final byte[] INET_DIAG_MSG_BYTES =
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 5a231fc..0958f11 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -21,7 +21,7 @@
import static android.system.OsConstants.AF_UNSPEC;
import static android.system.OsConstants.EACCES;
import static android.system.OsConstants.NETLINK_ROUTE;
-
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -55,6 +55,9 @@
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -65,19 +68,14 @@
@Test
public void testGetNeighborsQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final byte[] req = RtNetlinkNeighborMessage.newGetNeighborsRequest(TEST_SEQNO);
assertNotNull(req);
+ List<RtNetlinkNeighborMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkNeighborMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+
final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
final int targetSdk =
ctx.getPackageManager()
@@ -94,7 +92,8 @@
assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
ctxt.startsWith("u:r:network_stack:s0"));
try {
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
+ " target SDK version: " + targetSdk);
} catch (ErrnoException e) {
@@ -105,106 +104,70 @@
}
// Check that apps targeting lower API levels / running on older platforms succeed
- assertEquals(req.length,
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS));
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
- int neighMessageCount = 0;
- int doneMessageCount = 0;
-
- while (doneMessageCount == 0) {
- ByteBuffer response =
- NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TEST_TIMEOUT_MS);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- // Verify the messages at least appears minimally reasonable.
- while (response.remaining() > 0) {
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
- assertNotNull(msg);
- final StructNlMsgHdr hdr = msg.getHeader();
- assertNotNull(hdr);
-
- if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- doneMessageCount++;
- continue;
- }
-
- assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
- assertTrue(msg instanceof RtNetlinkNeighborMessage);
- assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
- assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), hdr.nlmsg_pid);
-
- neighMessageCount++;
- }
+ for (var msg : msgs) {
+ assertNotNull(msg);
+ final StructNlMsgHdr hdr = msg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+ assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+ assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
}
- assertEquals(1, doneMessageCount);
// TODO: make sure this test passes sanely in airplane mode.
- assertTrue(neighMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
}
@Test
public void testBasicWorkingGetAddrQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final int testSeqno = 8;
final byte[] req = newGetAddrRequest(testSeqno);
assertNotNull(req);
- final long timeout = 500;
- assertEquals(req.length, NetlinkUtils.sendMessage(fd, req, 0, req.length, timeout));
+ List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+ NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+ RtNetlinkAddressMessage.class, handleNlDumpMsg);
- int addrMessageCount = 0;
+ boolean ipv4LoopbackAddressFound = false;
+ boolean ipv6LoopbackAddressFound = false;
+ final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+ final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
- while (true) {
- ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
+ for (var msg : msgs) {
assertNotNull(msg);
final StructNlMsgHdr nlmsghdr = msg.getHeader();
assertNotNull(nlmsghdr);
-
- if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- break;
- }
-
assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
assertTrue(msg instanceof RtNetlinkAddressMessage);
- addrMessageCount++;
-
- // From the query response we can see the RTM_NEWADDR messages representing for IPv4
- // and IPv6 loopback address: 127.0.0.1 and ::1.
+ // When parsing the full response we can see the RTM_NEWADDR messages representing for
+ // IPv4 and IPv6 loopback address: 127.0.0.1 and ::1 and non-loopback addresses.
final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
assertTrue(
"Non-IP address family: " + ifaMsg.family,
ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
- assertTrue(ipAddress.isLoopbackAddress());
+ assertNotNull(ipAddress);
+
+ if (ipAddress.equals(loopbackIpv4)) {
+ ipv4LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
+ if (ipAddress.equals(loopbackIpv6)) {
+ ipv6LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
}
- assertTrue(addrMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
+ // Check ipv4 and ipv6 loopback addresses are in the output
+ assertTrue(ipv4LoopbackAddressFound && ipv6LoopbackAddressFound);
}
/** A convenience method to create an RTM_GETADDR request message. */
@@ -228,4 +191,17 @@
return bytes;
}
+
+ @Test
+ public void testGetIpv6MulticastRoutes_doesNotThrow() {
+ var multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ assertNotNull(route);
+ assertEquals("Route is not IP6MR: " + route,
+ RTNL_FAMILY_IP6MR, route.getRtmFamily());
+ assertNotNull("Route doesn't contain source: " + route, route.getSource());
+ assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
+ }
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
index 9881653..50b8278 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -16,7 +16,9 @@
package com.android.net.module.util.netlink;
+import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.NETLINK_ROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,7 @@
import java.net.Inet6Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -127,6 +130,72 @@
assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
}
+ private static final String RTM_GETROUTE_MULTICAST_IPV6_HEX =
+ "1C0000001A0001030000000000000000" // struct nlmsghr
+ + "810000000000000000000000"; // struct rtmsg
+
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_HEX =
+ "88000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "08000F00FE000000" // RTA_TABLE
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0009000800000111000000" // RTA_MULTIPATH
+ + "1C00110001000000000000009400000000000000" // RTA_STATS
+ + "0000000000000000"
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+
+ @Test
+ public void testParseRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final StructNlMsgHdr hdr = routeMsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(136, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+
+ final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+ assertNotNull(rtmsg);
+ assertEquals((byte) 129, (byte) rtmsg.family);
+ assertEquals(128, rtmsg.dstLen);
+ assertEquals(128, rtmsg.srcLen);
+ assertEquals(0xFE, rtmsg.table);
+
+ assertEquals(routeMsg.getSource(),
+ new IpPrefix("fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea/128"));
+ assertEquals(routeMsg.getDestination(), new IpPrefix("ff04::1234/128"));
+ assertEquals(20, routeMsg.getIifIndex());
+ assertEquals(60060, routeMsg.getSinceLastUseMillis());
+ }
+
+ // NEWROUTE message for multicast IPv6 with the packed attributes
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX =
+ "58000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+ @Test
+ public void testPackRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ final ByteBuffer packBuffer = ByteBuffer.allocate(88);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ routeMsg.pack(packBuffer);
+ assertEquals(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX,
+ HexDump.toHexString(packBuffer.array()));
+ }
+
private static final String RTM_NEWROUTE_TRUNCATED_HEX =
"48000000180000060000000000000000" // struct nlmsghr
+ "0A400000FC02000100000000" // struct rtmsg
@@ -220,10 +289,79 @@
+ "scope: 0, type: 1, flags: 0}, "
+ "destination{2001:db8:1::}, "
+ "gateway{fe80::1}, "
- + "ifindex{735}, "
+ + "oifindex{735}, "
+ "rta_cacheinfo{clntref: 0, lastuse: 0, expires: 59998, error: 0, used: 0, "
+ "id: 0, ts: 0, tsage: 0} "
+ "}";
assertEquals(expected, routeMsg.toString());
}
+
+ @Test
+ public void testToString_RtmGetRoute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_GETROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{28}, nlmsg_type{26(RTM_GETROUTE)}, "
+ + "nlmsg_flags{769(NLM_F_REQUEST|NLM_F_DUMP)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 0, srcLen: 0, tos: 0, table: 0, protocol: 0, "
+ + "scope: 0, type: 0, flags: 0}, "
+ + "destination{::}, "
+ + "gateway{}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testToString_RtmNewRouteMulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+ + "nlmsg_flags{2(NLM_F_MULTI)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 128, srcLen: 128, tos: 0, table: 254, protocol: 17, "
+ + "scope: 0, type: 5, flags: 0}, "
+ + "source{fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea}, "
+ + "destination{ff04::1234}, "
+ + "gateway{}, "
+ + "iifindex{20}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "sinceLastUseMillis{60060}"
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testGetRtmFamily_RTNL_FAMILY_IP6MR() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(RTNL_FAMILY_IP6MR, routeMsg.getRtmFamily());
+ }
+
+ @Test
+ public void testGetRtmFamily_AF_INET6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(AF_INET6, routeMsg.getRtmFamily());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index af3fac2..4c3fde6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -92,4 +92,26 @@
assertNull(integer3);
assertEquals(int3, 0x08 /* default value */);
}
+
+ @Test
+ public void testGetValueAsLong() {
+ final Long input = 1234567L;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetValueAsLong_malformed() {
+ final int input = 1234567;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertNull(output);
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
index b7f68c6..a0d8b8c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
@@ -16,6 +16,7 @@
package com.android.net.module.util.netlink;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static org.junit.Assert.fail;
import android.system.OsConstants;
@@ -48,10 +49,14 @@
public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+ return makeStructNlMsgHdr(type, TEST_NLMSG_FLAGS);
+ }
+
+ private StructNlMsgHdr makeStructNlMsgHdr(short type, short flags) {
final StructNlMsgHdr struct = new StructNlMsgHdr();
struct.nlmsg_len = TEST_NLMSG_LEN;
struct.nlmsg_type = type;
- struct.nlmsg_flags = TEST_NLMSG_FLAGS;
+ struct.nlmsg_flags = flags;
struct.nlmsg_seq = TEST_NLMSG_SEQ;
struct.nlmsg_pid = TEST_NLMSG_PID;
return struct;
@@ -62,6 +67,11 @@
fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
}
+ private static void assertNotContains(String actualValue, String unexpectedSubstring) {
+ if (!actualValue.contains(unexpectedSubstring)) return;
+ fail("\"" + actualValue + "\" contains \"" + unexpectedSubstring + "\"");
+ }
+
@Test
public void testToString() {
StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
@@ -99,4 +109,31 @@
assertContains(s, TEST_NLMSG_PID_STR);
assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
}
+
+ @Test
+ public void testToString_flags_dumpRequest() {
+ final short flags = StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP;
+ StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+ String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+ assertContains(s, "RTM_GETROUTE");
+ assertContains(s, "NLM_F_REQUEST");
+ assertContains(s, "NLM_F_DUMP");
+ // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+ assertNotContains(s, "NLM_F_MATCH");
+ assertNotContains(s, "NLM_F_ROOT");
+ }
+
+ @Test
+ public void testToString_flags_root() {
+ final short flags = StructNlMsgHdr.NLM_F_ROOT;
+ StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+ String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+ assertContains(s, "NLM_F_ROOT");
+ // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+ assertNotContains(s, "NLM_F_DUMP");
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
new file mode 100644
index 0000000..a83fc36
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
@@ -0,0 +1,102 @@
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMf6cctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* originPort */
+ 0, 0, 0, 0, /* originFlowinfo */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* originAddress */
+ 0, 0, 0, 0, /* originScopeId */
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* groupPort */
+ 0, 0, 0, 0, /* groupFlowinfo*/
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /*groupAddress*/
+ 0, 0, 0, 0, /* groupScopeId*/
+ 1, 0, /* mf6ccParent */
+ 0, 0, /* padding */
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* mf6ccIfset */
+ };
+
+ private static final int OIF = 10;
+ private static final byte[] OIFSET_BYTES = new byte[] {
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address DESTINATION =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testConstructor() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+
+ assertTrue(Arrays.equals(SOURCE.getAddress(), mf6cctl.originAddress));
+ assertTrue(Arrays.equals(DESTINATION.getAddress(), mf6cctl.groupAddress));
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals(OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testConstructor_tooBigOifIndex_throwsIllegalArgumentException()
+ throws UnknownHostException {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(1000);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new StructMf6cctl(SOURCE, DESTINATION, 1, oifset));
+ }
+
+ @Test
+ public void testParseMf6cctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMf6cctl mf6cctl = StructMf6cctl.parse(StructMf6cctl.class, buf);
+
+ assertEquals(AF_INET6, mf6cctl.originFamily);
+ assertEquals(AF_INET6, mf6cctl.groupFamily);
+ assertArrayEquals(SOURCE.getAddress(), mf6cctl.originAddress);
+ assertArrayEquals(DESTINATION.getAddress(), mf6cctl.groupAddress);
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals("mf6ccIfset = " + Arrays.toString(mf6cctl.mf6ccIfset),
+ OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+ byte[] bytes = mf6cctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
new file mode 100644
index 0000000..75196e4
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
@@ -0,0 +1,70 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMif6ctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 1, 0, /* mif6cMifi */
+ 0, /* mif6cFlags */
+ 1, /* vifcThreshold*/
+ 20, 0, /* mif6cPifi */
+ 0, 0, 0, 0, /* vifcRateLimit */
+ 0, 0 /* padding */
+ };
+
+ @Test
+ public void testConstructor() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(10 /* mif6cMifi */,
+ (short) 11 /* mif6cFlags */,
+ (short) 12 /* vifcThreshold */,
+ 13 /* mif6cPifi */,
+ 14L /* vifcRateLimit */);
+
+ assertEquals(10, mif6ctl.mif6cMifi);
+ assertEquals(11, mif6ctl.mif6cFlags);
+ assertEquals(12, mif6ctl.vifcThreshold);
+ assertEquals(13, mif6ctl.mif6cPifi);
+ assertEquals(14, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testParseMif6ctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMif6ctl mif6ctl = StructMif6ctl.parse(StructMif6ctl.class, buf);
+
+ assertEquals(1, mif6ctl.mif6cMifi);
+ assertEquals(0, mif6ctl.mif6cFlags);
+ assertEquals(1, mif6ctl.vifcThreshold);
+ assertEquals(20, mif6ctl.mif6cPifi);
+ assertEquals(0, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(1 /* mif6cMifi */,
+ (short) 0 /* mif6cFlags */,
+ (short) 1 /* vifcThreshold */,
+ 20 /* mif6cPifi */,
+ (long) 0 /* vifcRateLimit */);
+
+ byte[] bytes = mif6ctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
new file mode 100644
index 0000000..f1b75a0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
@@ -0,0 +1,58 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMrt6MsgTest {
+
+ private static final byte[] MSG_BYTES = new byte[] {
+ 0, /* mbz = 0 */
+ 1, /* message type = MRT6MSG_NOCACHE */
+ 1, 0, /* mif u16 = 1 */
+ 0, 0, 0, 0, /* padding */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* source=2001:db8::1 */
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /* destination=ff05::1234 */
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address GROUP =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testParseMrt6Msg() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+
+ assertEquals(1, mrt6Msg.mif);
+ assertEquals(StructMrt6Msg.MRT6MSG_NOCACHE, mrt6Msg.msgType);
+ assertEquals(SOURCE, mrt6Msg.src);
+ assertEquals(GROUP, mrt6Msg.dst);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMrt6Msg msg = new StructMrt6Msg((byte) 0 /* mbz must be 0 */,
+ StructMrt6Msg.MRT6MSG_NOCACHE,
+ 1 /* mif */,
+ SOURCE,
+ GROUP);
+ byte[] bytes = msg.writeToBytes();
+
+ assertArrayEquals(MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5e5afb..a5c4fea 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -84,6 +84,13 @@
"host/**/*.kt",
],
libs: ["tradefed"],
- test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+ test_suites: [
+ "ats",
+ "device-tests",
+ "general-tests",
+ "cts",
+ "mts-networking",
+ "mcts-networking",
+ ],
data: [":ConnectivityTestPreparer"],
}
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index 585157f..57602f1 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -133,7 +133,16 @@
}
@Test
- fun testReadFromRecorder_manyUids() {
+ fun testReadFromRecorder_manyUids_useDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = false)
+ }
+
+ @Test
+ fun testReadFromRecorder_manyUids_useFastDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = true)
+ }
+
+ fun doTestReadFromRecorder_manyUids(useFastDataInput: Boolean) {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
testFilesAssets.forEach {
@@ -146,7 +155,9 @@
PREFIX_UID,
UID_COLLECTION_BUCKET_DURATION_MS,
false /* includeTags */,
- false /* wipeOnError */
+ false /* wipeOnError */,
+ useFastDataInput /* useFastDataInput */,
+ it
)
recorder.orLoadCompleteLocked
}
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index ffe0e91..79c4980 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -40,6 +41,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -114,6 +116,7 @@
NsdServiceInfo fullInfo = new NsdServiceInfo();
fullInfo.setServiceName("kitten");
fullInfo.setServiceType("_kitten._tcp");
+ fullInfo.setSubtypes(Set.of("_thread", "_matter"));
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setNetwork(new Network(123));
@@ -149,7 +152,7 @@
assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
}
- public void checkParcelable(NsdServiceInfo original) {
+ private static void checkParcelable(NsdServiceInfo original) {
// Write to parcel.
Parcel p = Parcel.obtain();
Bundle writer = new Bundle();
@@ -179,11 +182,20 @@
}
}
- public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+ private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
byte[] txtRecord = shouldBeEmpty.getTxtRecord();
if (txtRecord == null || txtRecord.length == 0) {
return;
}
fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
}
+
+ @Test
+ public void testSubtypesValidSubtypesSuccess() {
+ NsdServiceInfo info = new NsdServiceInfo();
+
+ info.setSubtypes(Set.of("_thread", "_matter"));
+
+ assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
+ }
}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index e55ba63..a0aafc6 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -43,6 +43,8 @@
test_suites: [
"cts",
"general-tests",
+ "mcts-tethering",
+ "mts-tethering",
"sts"
],
data: [
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..ab956bf 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -203,6 +203,7 @@
// Initial state
setBatterySaverMode(false);
setRestrictBackground(false);
+ setAppIdle(false);
// Get transports of the active network, this has to be done before changing meteredness,
// since wifi will be disconnected when changing from non-metered to metered.
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 9310888..3d53d6c 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -131,6 +131,7 @@
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index da4fe28..a9e3715 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -49,5 +49,7 @@
"general-tests",
"mts-dnsresolver",
"mts-networking",
+ "mcts-dnsresolver",
+ "mcts-networking",
],
}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4() throws Exception {
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6() throws Exception {
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index fe2f813..84b6745 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -626,6 +626,7 @@
}
}
agent.unregister()
+ callback.eventuallyExpect<Lost> { it.network == agent.network }
// callback will be unregistered in tearDown()
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
var downstreamIface: TestNetworkInterface? = null
var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
setIncludeTestInterfaces(true)
var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..a040201 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -43,6 +43,7 @@
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@@ -992,6 +993,130 @@
}
@Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
+ }
+
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
+ }
+
+ private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ if (useLegacySpecifier) {
+ si.subtypes = setOf("_subtype1")
+
+ // Test "_type._tcp.local,_subtype" syntax with the registration
+ si.serviceType = si.serviceType + ",_subtype2"
+ } else {
+ si.subtypes = setOf("_subtype1", "_subtype2")
+ }
+
+ val registrationRecord = NsdRegistrationRecord()
+
+ val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+ val subtype1DiscoveryRecord = NsdDiscoveryRecord()
+ val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+ val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+
+ // Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
+ // "<subtype>._sub._type._tcp.local".
+ nsdManager.discoverServices("_othersubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+ nsdManager.discoverServices("_subtype1.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord)
+
+ val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info1.subtypes.contains("_subtype1"))
+ val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info2.subtypes.contains("_subtype2"))
+ baseTypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+ // The subtype callback was registered later but called, no need for an extra delay
+ otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+ baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ // Sets 101 subtypes in total
+ val seq = generateSequence(1) { it + 1}
+ si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
+ si.serviceType = si.serviceType + ",_subtype"
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
+ fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ si.subtypes = setOf("")
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
fun testRegisterWithConflictDuringProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
@@ -1305,6 +1430,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
index 9de7f4d..8919666 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -213,7 +213,6 @@
assertFalse(isUidNetworkingBlocked(TEST_UID3))
}
- @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
fun testGetDataSaverEnabled() {
testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 0082af2..b71a46f 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,13 +16,6 @@
package android.net;
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -46,7 +39,6 @@
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -70,10 +62,7 @@
import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
-import android.net.ConnectivityManager.DataSaverStatusTracker;
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -527,44 +516,6 @@
+ " attempts", ref.get());
}
- @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
- @Test
- public void testDataSaverStatusTracker() {
- mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
- // Mock proper application info.
- doReturn(mCtx).when(mCtx).getApplicationContext();
- final ApplicationInfo mockAppInfo = new ApplicationInfo();
- mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
- doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
- // Enable data saver.
- doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
-
- final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
- // Verify the data saver status is correct right after initialization.
- assertTrue(tracker.getDataSaverEnabled());
-
- // Verify the tracker register receiver with expected intent filter.
- final ArgumentCaptor<IntentFilter> intentFilterCaptor =
- ArgumentCaptor.forClass(IntentFilter.class);
- verify(mCtx).registerReceiver(
- any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
- assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
- intentFilterCaptor.getValue().getAction(0));
-
- // Mock data saver status changed event and verify the tracker tracks the
- // status accordingly.
- doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertFalse(tracker.getDataSaverEnabled());
-
- doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertTrue(tracker.getDataSaverEnabled());
- }
-
private <T> void mockService(Class<T> clazz, String name, T service) {
doReturn(service).when(mCtx).getSystemService(name);
doReturn(name).when(mCtx).getSystemServiceName(clazz);
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index a6e9e95..81557f8 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -64,6 +64,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -90,7 +91,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsCollectionTest {
-
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TEST_FILE = "test.bin";
private static final String TEST_IMSI = "310260000000000";
private static final int TEST_SUBID = 1;
@@ -199,6 +201,33 @@
77017831L, 100995L, 35436758L, 92344L);
}
+ private InputStream getUidInputStreamFromRes(int uidRes) throws Exception {
+ final File testFile =
+ new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+ stageFile(uidRes, testFile);
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+ collection.readLegacyUid(testFile, true);
+
+ // now export into a unified format
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ collection.write(bos);
+ return new ByteArrayInputStream(bos.toByteArray());
+ }
+
+ @Test
+ public void testFastDataInputRead() throws Exception {
+ final NetworkStatsCollection legacyCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, false /* useFastDataInput */);
+ final NetworkStatsCollection fastReadCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, true /* useFastDataInput */);
+ final InputStream bis = getUidInputStreamFromRes(R.raw.netstats_uid_v4);
+ legacyCollection.read(bis);
+ bis.reset();
+ fastReadCollection.read(bis);
+ assertCollectionEntries(legacyCollection.getEntries(), fastReadCollection);
+ }
+
@Test
public void testStartEndAtomicBuckets() throws Exception {
final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
index fad11a3..7d039b6 100644
--- a/tests/unit/java/android/net/NetworkStatsRecorderTest.java
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -16,8 +16,17 @@
package com.android.server.net;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.any;
@@ -29,21 +38,31 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.os.DropBoxManager;
import androidx.test.filters.SmallTest;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import libcore.testing.io.TestIoUtils;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
@RunWith(DevSdkIgnoreRunner.class)
@@ -53,6 +72,8 @@
private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
private static final String TEST_PREFIX = "test";
+ private static final int TEST_UID1 = 1234;
+ private static final int TEST_UID2 = 1235;
@Mock private DropBoxManager mDropBox;
@Mock private NetworkStats.NonMonotonicObserver mObserver;
@@ -64,7 +85,8 @@
private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
- HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError,
+ false /* useFastDataInput */, null /* baseDir */);
}
@Test
@@ -85,4 +107,110 @@
// Verify that the rotator won't delete files.
verify(rotator, never()).deleteAll();
}
+
+ @Test
+ public void testFileReadingMetrics_empty() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_XT, 888, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 1 /* readIndex */,
+ 888 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+
+ // Write second time, verify the index increases.
+ logger.logRecorderFileReading(PREFIX_XT, 567, null /* statsDir */, collection,
+ true /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 2 /* readIndex */,
+ 567 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ true /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet identSet = new NetworkIdentitySet();
+ identSet.add(new NetworkIdentity.Builder().build());
+ // Empty entries will be skipped, put some ints to make sure they can be recorded.
+ entry.rxBytes = 1;
+
+ collection.recordData(identSet, TEST_UID1, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_FOREGROUND, TAG_NONE, 30, 60, entry);
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID, 123, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID,
+ 1 /* readIndex */,
+ 123 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 3 /* keys */,
+ 2 /* uids */,
+ 5 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics_fileAttributes() throws IOException {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+
+ // Create files for testing. Only the first and the third files should be counted,
+ // with total 26 (each char takes 2 bytes) bytes in the content.
+ final File statsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ write(statsDir, "uid_tag.1024-2048", "wanted");
+ write(statsDir, "uid_tag.1024-2048.backup", "");
+ write(statsDir, "uid_tag.2048-", "wanted2");
+ write(statsDir, "uid.2048-4096", "unwanted");
+ write(statsDir, "uid.2048-4096.backup", "unwanted2");
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID_TAG, 678, statsDir, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG,
+ 1 /* readIndex */,
+ 678 /* readLatencyMillis */,
+ 2 /* fileCount */,
+ 26 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ private void write(@NonNull File baseDir, @NonNull String name,
+ @NonNull String value) throws IOException {
+ final DataOutputStream out = new DataOutputStream(
+ new FileOutputStream(new File(baseDir, name)));
+ out.writeChars(value);
+ out.close();
+ }
}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..1cfdefe 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -196,6 +196,22 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -346,8 +362,19 @@
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
// Service registration
// - invalid arguments
@@ -358,7 +385,21 @@
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..5709ed1
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 com.android.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mDependencies;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+ @Test
+ public void testSetNetworkRequestRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+
+ NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+ 0, 1, NetworkRequest.Type.REQUEST
+ );
+
+ // This call will be used to calculate NR received time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+ NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ notMeteredWifiNetworkRequest, mDependencies);
+
+ // This call will be used to calculate NR removed time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateInfo.setNetworkRequestRemoved();
+ assertEquals(
+ nrEndTime - nrStartTime,
+ mNetworkRequestStateInfo.getNetworkRequestDurationMillis());
+ assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+ }
+
+ @Test
+ public void testCheckInitialState() {
+ NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+ mDependencies);
+ assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+ }
+}
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..17a0719
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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 com.android.metrics;
+
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.HandlerThread;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+ @Mock
+ private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ @Captor
+ private ArgumentCaptor<NetworkRequestStateInfo> mNetworkRequestStateInfoCaptor;
+ private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+ private HandlerThread mHandlerThread;
+ private static final int TEST_REQUEST_ID = 10;
+ private static final int TEST_PACKAGE_UID = 20;
+ private static final int TIMEOUT_MS = 30_000;
+ private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+ .setRequestorUid(TEST_PACKAGE_UID),
+ 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+ );
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+ Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+ .thenReturn(mHandlerThread);
+ mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+ mNRStateStatsDeps, mNRStateInfoDeps);
+ }
+
+ @Test
+ public void testNetworkRequestReceivedRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+ // This call will be used to calculate NR received time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(mNetworkRequestStateInfoCaptor.capture());
+
+ NetworkRequestStateInfo nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+ clearInvocations(mNRStateStatsDeps);
+ // This call will be used to calculate NR removed time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(mNetworkRequestStateInfoCaptor.capture());
+
+ nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+ }
+
+ @Test
+ public void testUnreceivedNetworkRequestRemoved() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testExistingNetworkRequestReceived() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ clearInvocations(mNRStateStatsDeps);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ }
+}
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 1618a62..8037542 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -33,6 +33,7 @@
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -74,6 +75,7 @@
import androidx.test.filters.SmallTest;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
import com.android.server.IpSecService.TunnelInterfaceRecord;
import com.android.testutils.DevSdkIgnoreRule;
@@ -85,6 +87,7 @@
import org.junit.runners.Parameterized;
import java.net.Inet4Address;
+import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collection;
@@ -149,6 +152,7 @@
private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList(
android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
android.Manifest.permission.NETWORK_STACK,
+ android.Manifest.permission.ACCESS_NETWORK_STATE,
PERMISSION_MAINLINE_NETWORK_STACK));
private void setAllowedPermissions(String... permissions) {
@@ -202,11 +206,13 @@
private IpSecService.Dependencies makeDependencies() throws RemoteException {
final IpSecService.Dependencies deps = mock(IpSecService.Dependencies.class);
when(deps.getNetdInstance(mTestContext)).thenReturn(mMockNetd);
+ when(deps.getIpSecXfrmController()).thenReturn(mMockXfrmCtrl);
return deps;
}
INetd mMockNetd;
PackageManager mMockPkgMgr;
+ IpSecXfrmController mMockXfrmCtrl;
IpSecService.Dependencies mDeps;
IpSecService mIpSecService;
Network fakeNetwork = new Network(0xAB);
@@ -235,6 +241,7 @@
@Before
public void setUp() throws Exception {
mMockNetd = mock(INetd.class);
+ mMockXfrmCtrl = mock(IpSecXfrmController.class);
mMockPkgMgr = mock(PackageManager.class);
mDeps = makeDependencies();
mIpSecService = new IpSecService(mTestContext, mDeps);
@@ -506,6 +513,32 @@
}
@Test
+ public void getTransformState() throws Exception {
+ XfrmNetlinkNewSaMessage mockXfrmNewSaMsg = mock(XfrmNetlinkNewSaMessage.class);
+ when(mockXfrmNewSaMsg.getBitmap()).thenReturn(new byte[512]);
+ when(mMockXfrmCtrl.ipSecGetSa(any(InetAddress.class), anyLong()))
+ .thenReturn(mockXfrmNewSaMsg);
+
+ // Create transform
+ IpSecConfig ipSecConfig = new IpSecConfig();
+ addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+ addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+ IpSecTransformResponse createTransformResp =
+ mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+ assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+ // Get transform state
+ mIpSecService.getTransformState(createTransformResp.resourceId);
+
+ // Verifications
+ verify(mMockXfrmCtrl)
+ .ipSecGetSa(
+ eq(InetAddresses.parseNumericAddress(mDestinationAddr)),
+ eq(Integer.toUnsignedLong(TEST_SPI)));
+ }
+
+ @Test
public void testReleaseOwnedSpi() throws Exception {
IpSecConfig ipSecConfig = new IpSecConfig();
addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
new file mode 100644
index 0000000..8c1f47f
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.android.server;
+
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_ESRCH_HEX;
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_NEW_SA_HEX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.InetAddresses;
+import android.system.ErrnoException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpSecXfrmControllerTest {
+ private static final InetAddress DEST_ADDRESS =
+ InetAddresses.parseNumericAddress("2001:db8::111");
+ private static final long SPI = 0xaabbccddL;
+ private static final int ESRCH = -3;
+
+ private IpSecXfrmController mXfrmController;
+ private FileDescriptor mDummyNetlinkSocket;
+
+ @Mock private IpSecXfrmController.Dependencies mMockDeps;
+
+ @Captor private ArgumentCaptor<byte[]> mRequestByteArrayCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mDummyNetlinkSocket = new FileDescriptor();
+
+ when(mMockDeps.newNetlinkSocket()).thenReturn(mDummyNetlinkSocket);
+ mXfrmController = new IpSecXfrmController(mMockDeps);
+ }
+
+ @Test
+ public void testStartStop() throws Exception {
+ mXfrmController.openNetlinkSocketIfNeeded();
+
+ verify(mMockDeps).newNetlinkSocket();
+ assertNotNull(mXfrmController.getNetlinkSocket());
+
+ mXfrmController.closeNetlinkSocketIfNeeded();
+ verify(mMockDeps).releaseNetlinkSocket(eq(mDummyNetlinkSocket));
+ assertNull(mXfrmController.getNetlinkSocket());
+ }
+
+ private static void injectRxMessage(IpSecXfrmController.Dependencies mockDeps, byte[] bytes)
+ throws Exception {
+ final ByteBuffer buff = ByteBuffer.wrap(bytes);
+ buff.order(ByteOrder.nativeOrder());
+
+ when(mockDeps.recvMessage(any(FileDescriptor.class))).thenReturn(buff);
+ }
+
+ @Test
+ public void testIpSecGetSa() throws Exception {
+ final int expectedReqLen = 40;
+ injectRxMessage(mMockDeps, XFRM_NEW_SA_HEX);
+
+ final NetlinkMessage netlinkMessage = mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+ final XfrmNetlinkNewSaMessage message = (XfrmNetlinkNewSaMessage) netlinkMessage;
+
+ // Verifications
+ assertEquals(SPI, message.getXfrmUsersaInfo().getSpi());
+ assertEquals(DEST_ADDRESS, message.getXfrmUsersaInfo().getDestAddress());
+
+ verify(mMockDeps).sendMessage(eq(mDummyNetlinkSocket), mRequestByteArrayCaptor.capture());
+ final byte[] request = mRequestByteArrayCaptor.getValue();
+ assertEquals(expectedReqLen, request.length);
+
+ verify(mMockDeps).recvMessage(eq(mDummyNetlinkSocket));
+ }
+
+ @Test
+ public void testIpSecGetSa_NlErrorMsg() throws Exception {
+ injectRxMessage(mMockDeps, XFRM_ESRCH_HEX);
+
+ try {
+ mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+ fail("Expected to fail with ESRCH ");
+ } catch (ErrnoException e) {
+ assertEquals(ESRCH, e.errno);
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
new file mode 100644
index 0000000..a2082c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.server;
+
+import com.android.net.module.util.HexDump;
+
+public class IpSecXfrmControllerTestHex {
+ private static final String XFRM_NEW_SA_HEX_STRING =
+ "2003000010000000000000003FE1D4B6"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000A00000000000000"
+ + "000000000000000020010DB800000000"
+ + "0000000000000111AABBCCDD32000000"
+ + "20010DB8000000000000000000000222"
+ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "FD464C65000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "024000000A0000000000000000000000"
+ + "5C000100686D61632873686131290000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000A000000055F01AC07E15E437"
+ + "115DDE0AEDD18A822BA9F81E60001400"
+ + "686D6163287368613129000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "A00000006000000055F01AC07E15E437"
+ + "115DDE0AEDD18A822BA9F81E58000200"
+ + "63626328616573290000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "800000006AED4975ADF006D65C76F639"
+ + "23A6265B1C0117004000000000000000"
+ + "00000000000000000000000000080000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000";
+ public static final byte[] XFRM_NEW_SA_HEX =
+ HexDump.hexStringToByteArray(XFRM_NEW_SA_HEX_STRING);
+
+ private static final String XFRM_ESRCH_HEX_STRING =
+ "3C0000000200000000000000A5060000"
+ + "FDFFFFFF280000001200010000000000"
+ + "0000000020010DB80000000000000000"
+ + "00000111AABBCCDD0A003200";
+ public static final byte[] XFRM_ESRCH_HEX = HexDump.hexStringToByteArray(XFRM_ESRCH_HEX_STRING);
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..4dc96f1 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -140,6 +140,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Queue;
+import java.util.Set;
// TODOs:
// - test client can send requests and receive replies
@@ -1117,7 +1118,8 @@
waitForIdle();
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
"Instance".equals(s.getServiceName())
- && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
+ && SERVICE_TYPE.equals(s.getServiceType())
+ && s.getSubtypes().equals(Set.of("_subtype"))), any());
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1223,7 +1225,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, regInfo)), any());
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1283,9 +1285,9 @@
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
- argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, service1)), any());
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
- eq(null) /* subtype */, any());
+ any());
}
@Test
@@ -1310,7 +1312,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)), eq(null) /* subtype */, any());
+ matches(info, regInfo)), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1358,7 +1360,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1388,8 +1390,7 @@
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))),
- eq(null) /* subtype */, any());
+ argThat(info -> info.getServiceName().equals("a".repeat(63))), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1453,14 +1454,22 @@
final String serviceType5 = "_TEST._999._tcp.";
final String serviceType6 = "_998._tcp.,_TEST";
final String serviceType7 = "_997._tcp,_TEST";
+ final String serviceType8 = "_997._tcp,_test1,_test2,_test3";
+ final String serviceType9 = "_test4._997._tcp,_test1,_test2,_test3";
assertNull(parseTypeAndSubtype(serviceType1));
assertNull(parseTypeAndSubtype(serviceType2));
assertNull(parseTypeAndSubtype(serviceType3));
- assertEquals(new Pair<>("_123._udp", null), parseTypeAndSubtype(serviceType4));
- assertEquals(new Pair<>("_999._tcp", "_TEST"), parseTypeAndSubtype(serviceType5));
- assertEquals(new Pair<>("_998._tcp", "_TEST"), parseTypeAndSubtype(serviceType6));
- assertEquals(new Pair<>("_997._tcp", "_TEST"), parseTypeAndSubtype(serviceType7));
+ assertEquals(new Pair<>("_123._udp", Collections.emptyList()),
+ parseTypeAndSubtype(serviceType4));
+ assertEquals(new Pair<>("_999._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType5));
+ assertEquals(new Pair<>("_998._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType6));
+ assertEquals(new Pair<>("_997._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType7));
+
+ assertEquals(new Pair<>("_997._tcp", List.of("_test1", "_test2", "_test3")),
+ parseTypeAndSubtype(serviceType8));
+ assertEquals(new Pair<>("_997._tcp", List.of("_test4")),
+ parseTypeAndSubtype(serviceType9));
}
@Test
@@ -1479,7 +1488,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1512,7 +1521,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
final Network wifiNetwork1 = new Network(123);
final Network wifiNetwork2 = new Network(124);
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 10a0982..4fcf8a8 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -142,81 +142,81 @@
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_INET_HEX =
// struct nlmsghdr.
- "14010000" + // length = 276
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
- "00000000" + // seqno
- "00000000" + // pid (0 == kernel)
+ "14010000" // length = 276
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP
+ + "00000000" // seqno
+ + "00000000" // pid (0 == kernel)
// struct inet_diag_req_v2
- "02" + // family = AF_INET
- "06" + // state
- "00" + // timer
- "00" + // retrans
+ + "02" // family = AF_INET
+ + "06" // state
+ + "00" // timer
+ + "00" // retrans
// inet_diag_sockid
- "DEA5" + // idiag_sport = 42462
- "71B9" + // idiag_dport = 47473
- "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
- "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
- "00000000" + // idiag_if
- "34ED000076270000" + // idiag_cookie = 43387759684916
- "00000000" + // idiag_expires
- "00000000" + // idiag_rqueue
- "00000000" + // idiag_wqueue
- "00000000" + // idiag_uid
- "00000000" + // idiag_inode
+ + "DEA5" // idiag_sport = 42462
+ + "71B9" // idiag_dport = 47473
+ + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+ + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+ + "00000000" // idiag_if
+ + "34ED000076270000" // idiag_cookie = 43387759684916
+ + "00000000" // idiag_expires
+ + "00000000" // idiag_rqueue
+ + "00000000" // idiag_wqueue
+ + "39300000" // idiag_uid = 12345
+ + "00000000" // idiag_inode
// rtattr
- "0500" + // len = 5
- "0800" + // type = 8
- "00000000" + // data
- "0800" + // len = 8
- "0F00" + // type = 15(INET_DIAG_MARK)
- "850A0C00" + // data, socket mark=789125
- "AC00" + // len = 172
- "0200" + // type = 2(INET_DIAG_INFO)
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "AC00" // len = 172
+ + "0200" // type = 2(INET_DIAG_INFO)
// tcp_info
- "01" + // state = TCP_ESTABLISHED
- "00" + // ca_state = TCP_CA_OPEN
- "05" + // retransmits = 5
- "00" + // probes = 0
- "00" + // backoff = 0
- "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
- "88" + // wscale = 8
- "00" + // delivery_rate_app_limited = 0
- "4A911B00" + // rto = 1806666
- "00000000" + // ato = 0
- "2E050000" + // sndMss = 1326
- "18020000" + // rcvMss = 536
- "00000000" + // unsacked = 0
- "00000000" + // acked = 0
- "00000000" + // lost = 0
- "00000000" + // retrans = 0
- "00000000" + // fackets = 0
- "BB000000" + // lastDataSent = 187
- "00000000" + // lastAckSent = 0
- "BB000000" + // lastDataRecv = 187
- "BB000000" + // lastDataAckRecv = 187
- "DC050000" + // pmtu = 1500
- "30560100" + // rcvSsthresh = 87600
- "3E2C0900" + // rttt = 601150
- "1F960400" + // rttvar = 300575
- "78050000" + // sndSsthresh = 1400
- "0A000000" + // sndCwnd = 10
- "A8050000" + // advmss = 1448
- "03000000" + // reordering = 3
- "00000000" + // rcvrtt = 0
- "30560100" + // rcvspace = 87600
- "00000000" + // totalRetrans = 0
- "53AC000000000000" + // pacingRate = 44115
- "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615
- "0100000000000000" + // bytesAcked = 1
- "0000000000000000" + // bytesReceived = 0
- "0A000000" + // SegsOut = 10
- "00000000" + // SegsIn = 0
- "00000000" + // NotSentBytes = 0
- "3E2C0900" + // minRtt = 601150
- "00000000" + // DataSegsIn = 0
- "00000000" + // DataSegsOut = 0
- "0000000000000000"; // deliverRate = 0
+ + "01" // state = TCP_ESTABLISHED
+ + "00" // ca_state = TCP_CA_OPEN
+ + "05" // retransmits = 5
+ + "00" // probes = 0
+ + "00" // backoff = 0
+ + "07" // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+ + "88" // wscale = 8
+ + "00" // delivery_rate_app_limited = 0
+ + "4A911B00" // rto = 1806666
+ + "00000000" // ato = 0
+ + "2E050000" // sndMss = 1326
+ + "18020000" // rcvMss = 536
+ + "00000000" // unsacked = 0
+ + "00000000" // acked = 0
+ + "00000000" // lost = 0
+ + "00000000" // retrans = 0
+ + "00000000" // fackets = 0
+ + "BB000000" // lastDataSent = 187
+ + "00000000" // lastAckSent = 0
+ + "BB000000" // lastDataRecv = 187
+ + "BB000000" // lastDataAckRecv = 187
+ + "DC050000" // pmtu = 1500
+ + "30560100" // rcvSsthresh = 87600
+ + "3E2C0900" // rttt = 601150
+ + "1F960400" // rttvar = 300575
+ + "78050000" // sndSsthresh = 1400
+ + "0A000000" // sndCwnd = 10
+ + "A8050000" // advmss = 1448
+ + "03000000" // reordering = 3
+ + "00000000" // rcvrtt = 0
+ + "30560100" // rcvspace = 87600
+ + "00000000" // totalRetrans = 0
+ + "53AC000000000000" // pacingRate = 44115
+ + "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615
+ + "0100000000000000" // bytesAcked = 1
+ + "0000000000000000" // bytesReceived = 0
+ + "0A000000" // SegsOut = 10
+ + "00000000" // SegsIn = 0
+ + "00000000" // NotSentBytes = 0
+ + "3E2C0900" // minRtt = 601150
+ + "00000000" // DataSegsIn = 0
+ + "00000000" // DataSegsOut = 0
+ + "0000000000000000"; // deliverRate = 0
private static final String SOCK_DIAG_NO_TCP_INET_HEX =
// struct nlmsghdr
"14000000" // length = 20
@@ -427,6 +427,16 @@
}
@Test
+ public void testIsAnyTcpSocketConnected_noTargetUidSocket() throws Exception {
+ setupResponseWithSocketExisting();
+ // Configured uid(12345) is not in the VPN range.
+ assertFalse(visibleOnHandlerThread(mTestHandler,
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(
+ TEST_NETID,
+ new ArraySet<>(Arrays.asList(new Range<>(99999, 99999))))));
+ }
+
+ @Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 46e9e45..c9cece0 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -2979,7 +2979,7 @@
null /* iface */, RTN_UNREACHABLE));
assertEquals(expectedRoutes, lp.getRoutes());
- verify(mMockNetworkAgent).unregister();
+ verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f0cb6df..121f844 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -68,6 +68,7 @@
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_INTERFACE1 = "test_iface1"
private val TEST_INTERFACE2 = "test_iface2"
private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
@@ -80,6 +81,13 @@
network = TEST_NETWORK_1
}
+private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val LONG_SERVICE_1 =
NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
port = 12345
@@ -93,6 +101,14 @@
network = null
}
+private val ALL_NETWORKS_SERVICE_SUBTYPE =
+ NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = null
+}
+
private val ALL_NETWORKS_SERVICE_2 =
NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
port = 12345
@@ -189,7 +205,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -247,14 +263,14 @@
}
@Test
- fun testAddService_AllNetworks() {
+ fun testAddService_AllNetworksWithSubType() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
- verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
+ verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
socketCbCaptor.capture())
val socketCb = socketCbCaptor.value
@@ -270,9 +286,9 @@
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -286,7 +302,7 @@
mockInterfaceAdvertiser2, SERVICE_ID_1) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
// Services are conflicted.
postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
@@ -323,7 +339,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -331,18 +347,18 @@
// Register a service with the same name on all networks (name conflict)
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -378,15 +394,15 @@
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) }, eq(null))
+ argThat { it.matches(SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) }, eq(null))
+ argThat { it.matches(expectedRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+ argThat { it.matches(LONG_SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) }, eq(null))
+ argThat { it.matches(expectedLongRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
+ argThat { it.matches(expectedCaseInsensitiveRenamed) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -411,7 +427,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -420,29 +436,28 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
+ argThat { it.matches(ALL_NETWORKS_SERVICE) })
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, TEST_SUBTYPE,
- updateOptions) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
- verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
+ verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(TEST_SUBTYPE))
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
}
@Test
@@ -451,7 +466,7 @@
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
verify(mockDeps, times(1)).generateHostname()
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index f85d71d..0c04bff 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -67,6 +67,13 @@
port = 12345
}
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+ subtypes = setOf("_sub")
+ serviceType = "_testservice._tcp"
+ serviceName = "MyTestService"
+ port = 12345
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsInterfaceAdvertiserTest {
@@ -119,7 +126,7 @@
knownServices.add(inv.getArgument(0))
-1
- }.`when`(repository).addService(anyInt(), any(), any())
+ }.`when`(repository).addService(anyInt(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -277,10 +284,9 @@
@Test
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
- verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
+ verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
}
@@ -288,9 +294,9 @@
@Test
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ val subTypes = setOf("_sub")
+ advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober, never()).startProbing(any())
@@ -302,8 +308,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo, null /* subtype */)
- verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+ advertiser.addService(serviceId, serviceInfo)
+ verify(repository).addService(serviceId, serviceInfo)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index c74e330..196f73f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -27,6 +27,7 @@
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.util.Collections
@@ -35,6 +36,7 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
@@ -46,6 +48,14 @@
private const val TEST_SERVICE_ID_3 = 44
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
+// RFC6762 10. Resource Record TTL Values and Cache Coherency
+// The recommended TTL value for Multicast DNS resource records with a host name as the resource
+// record's name (e.g., A, AAAA, HINFO) or a host name contained within the resource record's rdata
+// (e.g., SRV, reverse mapping PTR record) SHOULD be 120 seconds. The recommended TTL value for
+// other Multicast DNS resource records is 75 minutes.
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -95,8 +105,7 @@
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -119,7 +128,7 @@
assertEquals(MdnsServiceRecord(expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */, 0 /* serviceWeight */,
TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
@@ -131,10 +140,10 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
}
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
}
}
@@ -144,10 +153,10 @@
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
- repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
+ repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
}
- repository.updateService(TEST_SERVICE_ID_1, TEST_SUBTYPE)
+ repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
@@ -175,9 +184,9 @@
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
}
}
@@ -186,7 +195,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -229,9 +238,10 @@
}
@Test
- fun testExitAnnouncements_WithSubtype() {
+ fun testExitAnnouncements_WithSubtypes() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
@@ -245,7 +255,7 @@
assertEquals(0, packet.authorityRecords.size)
assertEquals(0, packet.additionalRecords.size)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
MdnsPointerRecord(
arrayOf("_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */,
@@ -258,7 +268,12 @@
false /* cacheFlush */,
0L /* ttlMillis */,
arrayOf("MyTestService", "_testservice", "_tcp", "local")),
- ), packet.answers)
+ MdnsPointerRecord(
+ arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
repository.removeService(TEST_SERVICE_ID_1)
assertEquals(0, repository.servicesCount)
@@ -272,7 +287,7 @@
repository.exitService(TEST_SERVICE_ID_1)
assertEquals(TEST_SERVICE_ID_1,
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -283,7 +298,7 @@
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- TEST_SUBTYPE)
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
@@ -294,12 +309,13 @@
val serviceType = arrayOf("_testservice", "_tcp", "local")
val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+ val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
// Reverse address and address records for the hostname
MdnsPointerRecord(v4AddrRev,
0L /* receiptTimeMillis */,
@@ -346,6 +362,13 @@
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
+ MdnsPointerRecord(
+ serviceSubtype2,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -367,8 +390,7 @@
0L /* receiptTimeMillis */,
false /* cacheFlush */,
4500000L /* ttlMillis */,
- serviceType)
- ), packet.answers)
+ serviceType))
assertContentEquals(listOf(
MdnsNsecRecord(v4AddrRev,
@@ -484,19 +506,20 @@
@Test
fun testGetReply() {
- doGetReplyTest(subtype = null)
+ doGetReplyTest(queryWithSubtype = false)
}
@Test
fun testGetReply_WithSubtype() {
- doGetReplyTest(TEST_SUBTYPE)
+ doGetReplyTest(queryWithSubtype = true)
}
- private fun doGetReplyTest(subtype: String?) {
+ private fun doGetReplyTest(queryWithSubtype: Boolean) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
- val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
- else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
+ else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
@@ -509,9 +532,6 @@
assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
- // TTLs as per RFC6762 10.
- val longTtl = 4_500_000L
- val shortTtl = 120_000L
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
assertEquals(listOf(
@@ -519,7 +539,7 @@
queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- longTtl,
+ LONG_TTL,
serviceName),
), reply.answers)
@@ -528,13 +548,13 @@
serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- longTtl,
+ LONG_TTL,
listOf() /* entries */),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- shortTtl,
+ SHORT_TTL,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
@@ -543,32 +563,32 @@
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- shortTtl,
+ SHORT_TTL,
TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- shortTtl,
+ SHORT_TTL,
TEST_ADDRESSES[1].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- shortTtl,
+ SHORT_TTL,
TEST_ADDRESSES[2].address),
MdnsNsecRecord(
serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- longTtl,
+ LONG_TTL,
serviceName /* nextDomain */,
intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
MdnsNsecRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- shortTtl,
+ SHORT_TTL,
TEST_HOSTNAME /* nextDomain */,
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
), reply.additionalAnswers)
@@ -577,8 +597,8 @@
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@@ -605,8 +625,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@@ -633,8 +653,8 @@
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -662,8 +682,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -718,8 +738,7 @@
MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -746,7 +765,7 @@
expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
@@ -755,33 +774,300 @@
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[1].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[2].address)
), packet.authorityRecords)
assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
}
+
+ private fun doGetReplyWithAnswersTest(
+ questions: List<MdnsRecord>,
+ knownAnswers: List<MdnsRecord>,
+ replyAnswers: List<MdnsRecord>,
+ additionalAnswers: List<MdnsRecord>,
+ expectReply: Boolean
+ ) {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ MdnsFeatureFlags.newBuilder().setIsKnownAnswerSuppressionEnabled(true).build())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+
+ if (!expectReply) {
+ assertNull(reply)
+ return
+ }
+
+ assertNotNull(reply)
+ // Source address is IPv4
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
+ assertEquals(replyAnswers, reply.answers)
+ assertEquals(additionalAnswers, reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_TtlLessThanHalf() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ (LONG_TTL / 2 - 1000L),
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnotherAnswer() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName))
+ val replyAnswers = listOf(MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ val additionalAnswers = listOf(
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions_NoReply() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(
+ MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL - 15_000L,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */)
+ }
}
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
- subtype: String? = null,
+ subtypes: Set<String> = setOf(),
): AnnouncementInfo {
updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo, subtype)
+ serviceInfo.setSubtypes(subtypes)
+ addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
new file mode 100644
index 0000000..9e2933f
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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 com.android.server.connectivity.mdns
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val TEST_PORT = 12345
+private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsReplySenderTest {
+ private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ private val serviceType = arrayOf("_testservice", "_tcp", "local")
+ private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+ private val hostAddresses = listOf(
+ LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::222"), 64))
+ private val answers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName))
+ private val additionalAnswers = listOf(
+ MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */, SHORT_TTL,
+ hostname /* nextDomain */, intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ private val thread = HandlerThread(MdnsReplySenderTest::class.simpleName)
+ private val socket = mock(MdnsInterfaceSocket::class.java)
+ private val buffer = ByteArray(1500)
+ private val sharedLog = SharedLog(MdnsReplySenderTest::class.simpleName)
+ private val deps = mock(MdnsReplySender.Dependencies::class.java)
+ private val handler by lazy { Handler(thread.looper) }
+ private val replySender by lazy {
+ MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */, deps)
+ }
+
+ @Before
+ fun setUp() {
+ thread.start()
+ doReturn(true).`when`(socket).hasJoinedIpv4()
+ doReturn(true).`when`(socket).hasJoinedIpv6()
+ }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
+ val future = CompletableFuture<T>()
+ handler.post {
+ future.complete(functor())
+ }
+ return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun sendNow(packet: MdnsPacket, destination: InetSocketAddress):
+ Unit = runningOnHandlerAndReturn { replySender.sendNow(packet, destination) }
+
+ private fun queueReply(reply: MdnsReplyInfo):
+ Unit = runningOnHandlerAndReturn { replySender.queueReply(reply) }
+
+ @Test
+ fun testSendNow() {
+ val packet = MdnsPacket(0x8400,
+ listOf() /* questions */,
+ answers,
+ listOf() /* authorityRecords */,
+ additionalAnswers)
+ sendNow(packet, IPV4_SOCKET_ADDR)
+ verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
+ }
+
+ @Test
+ fun testQueueReply() {
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR)
+ val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+ val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+ queueReply(reply)
+ verify(deps).sendMessageDelayed(handlerCaptor.capture(), messageCaptor.capture(), eq(20L))
+
+ val realHandler = handlerCaptor.value
+ val delayMessage = messageCaptor.value
+ realHandler.sendMessage(delayMessage)
+ verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(argThat{
+ it.socketAddress.equals(IPV4_SOCKET_ADDR)
+ })
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
new file mode 100644
index 0000000..c26ec53
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 com.android.server
+
+import android.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverBeforeV() {
+ val inOrder = inOrder(bpfNetMaps)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ }
+
+ // Data Saver Status is updated from platform code in V+.
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverAboveU() {
+ listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+ RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+ mockDataSaverStatus(it)
+ verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+ }
+ }
+
+ private fun mockDataSaverStatus(status: Int) {
+ doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+ // While the production code dispatches the intent on the handler thread,
+ // The test would dispatch the intent in the caller thread. Make it dispatch
+ // on the handler thread to match production behavior.
+ visibleOnHandlerThread(csHandler) {
+ context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+ }
+ waitForIdle()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index ad21bf5..dd0706b 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -154,6 +154,7 @@
val newLnc = LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build()
localAgent.sendLocalNetworkConfig(newLnc)
@@ -196,6 +197,7 @@
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
.build()),
@@ -250,6 +252,7 @@
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build()),
score = FromS(NetworkScore.Builder()
@@ -296,6 +299,7 @@
val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build())
val localScore = FromS(NetworkScore.Builder().build())
@@ -348,6 +352,7 @@
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
.build()),
@@ -377,6 +382,7 @@
val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_DUN)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build())
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
@@ -504,6 +510,7 @@
val lnc = FromS(LocalNetworkConfig.Builder().apply {
if (haveUpstream) {
setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+ private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+ private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+ .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+ @Before
+ fun setup() {
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+ }
+
+ @Test
+ fun testRequestTypeNRProduceMetrics() {
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+
+ verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+
+ @Test
+ fun testListenTypeNRProduceNoMetrics() {
+ cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+ }
+
+ @Test
+ fun testRemoveRequestTypeNRProduceMetrics() {
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+
+ cm.unregisterNetworkCallback(cb)
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 958c4f2..5c9a762 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -54,6 +54,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.metrics.NetworkRequestStateStatsMetrics
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
@@ -157,6 +158,7 @@
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
+ val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
@@ -197,6 +199,9 @@
MultinetworkPolicyTracker(c, h, r,
MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+ override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+ this@CSTest.networkRequestStateStatsMetrics
+
// All queried features must be mocked, because the test cannot hold the
// READ_DEVICE_CONFIG permission and device config utils use static methods for
// checking permissions.
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7a4dfed..1ee3f9d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,6 +67,8 @@
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+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;
@@ -169,7 +171,6 @@
import java.io.File;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -284,9 +285,14 @@
private @Mock PersistentInt mImportLegacyAttemptsCounter;
private @Mock PersistentInt mImportLegacySuccessesCounter;
private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private int mFastDataInputTargetAttempts = 0;
+ private @Mock PersistentInt mFastDataInputSuccessesCounter;
+ private @Mock PersistentInt mFastDataInputFallbacksCounter;
+ private String mCompareStatsResult = null;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
private HandlerThread mObserverHandlerThread;
+ final TestDependencies mDeps = new TestDependencies();
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -369,7 +375,6 @@
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
- final NetworkStatsService.Dependencies deps = makeDependencies();
// Create a separate thread for observers to run on. This thread cannot be the same
// as the handler thread, because the observer callback is fired on this thread, and
// it should not be blocked by client code. Additionally, creating the observers
@@ -384,7 +389,7 @@
}
};
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, statsObservers, deps);
+ mClock, mSettings, mStatsFactory, statsObservers, mDeps);
mElapsedRealtime = 0L;
@@ -423,132 +428,150 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
- @NonNull
- private NetworkStatsService.Dependencies makeDependencies() {
- return new NetworkStatsService.Dependencies() {
- @Override
- public File getLegacyStatsDir() {
- return mLegacyStatsDir;
- }
+ class TestDependencies extends NetworkStatsService.Dependencies {
+ private int mCompareStatsInvocation = 0;
- @Override
- public File getOrCreateStatsDir() {
- return mStatsDir;
- }
+ @Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
- @Override
- public boolean getStoreFilesInApexData() {
- return mStoreFilesInApexData;
- }
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
- @Override
- public int getImportLegacyTargetAttempts() {
- return mImportLegacyTargetAttempts;
- }
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
- @Override
- public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
- @androidx.annotation.NonNull String name) throws IOException {
- switch (name) {
- case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
- return mImportLegacyAttemptsCounter;
- case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
- return mImportLegacySuccessesCounter;
- case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
- return mImportLegacyFallbacksCounter;
- default:
- throw new IllegalArgumentException("Unknown counter name: " + name);
- }
- }
+ @Override
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
- @Override
- public NetworkStatsCollection readPlatformCollection(
- @NonNull String prefix, long bucketDuration) {
- return mPlatformNetworkStatsCollection.get(prefix);
- }
+ @Override
+ public int getUseFastDataInputTargetAttempts() {
+ return mFastDataInputTargetAttempts;
+ }
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
+ @Override
+ public String compareStats(NetworkStatsCollection a, NetworkStatsCollection b,
+ boolean allowKeyChange) {
+ mCompareStatsInvocation++;
+ return mCompareStatsResult;
+ }
- @Override
- public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
- @NonNull Context context, @NonNull Executor executor,
- @NonNull NetworkStatsService service) {
+ int getCompareStatsInvocation() {
+ return mCompareStatsInvocation;
+ }
- return mNetworkStatsSubscriptionsMonitor;
+ @Override
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) {
+ switch (name) {
+ case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+ return mImportLegacyAttemptsCounter;
+ case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+ return mImportLegacySuccessesCounter;
+ case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+ return mImportLegacyFallbacksCounter;
+ case NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME:
+ return mFastDataInputSuccessesCounter;
+ case NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME:
+ return mFastDataInputFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
}
+ }
- @Override
- public ContentObserver makeContentObserver(Handler handler,
- NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
- mHandler = handler;
- return mContentObserver = super.makeContentObserver(handler, settings, monitor);
- }
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
- @Override
- public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
- return mLocationPermissionChecker;
- }
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
- @Override
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return mBpfInterfaceMapUpdater;
- }
+ @Override
+ public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+ @NonNull Context context, @NonNull Executor executor,
+ @NonNull NetworkStatsService service) {
- @Override
- public IBpfMap<S32, U8> getUidCounterSetMap() {
- return mUidCounterSetMap;
- }
+ return mNetworkStatsSubscriptionsMonitor;
+ }
- @Override
- public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
- return mCookieTagMap;
- }
+ @Override
+ public ContentObserver makeContentObserver(Handler handler,
+ NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+ mHandler = handler;
+ return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
- return mStatsMapA;
- }
+ @Override
+ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+ return mLocationPermissionChecker;
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
- return mStatsMapB;
- }
+ @Override
+ public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+ @NonNull Context ctx, @NonNull Handler handler) {
+ return mBpfInterfaceMapUpdater;
+ }
- @Override
- public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
- return mAppUidStatsMap;
- }
+ @Override
+ public IBpfMap<S32, U8> getUidCounterSetMap() {
+ return mUidCounterSetMap;
+ }
- @Override
- public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
- return mIfaceStatsMap;
- }
+ @Override
+ public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+ return mCookieTagMap;
+ }
- @Override
- public boolean isDebuggable() {
- return mIsDebuggable == Boolean.TRUE;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+ return mStatsMapA;
+ }
- @Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+ return mStatsMapB;
+ }
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return mSkDestroyListener;
- }
+ @Override
+ public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+ return mAppUidStatsMap;
+ }
- @Override
- public boolean supportEventLogger(@NonNull Context cts) {
- return true;
- }
- };
+ @Override
+ public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
+ return mIfaceStatsMap;
+ }
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
+
+ @Override
+ public BpfNetMaps makeBpfNetMaps(Context ctx) {
+ return mBpfNetMaps;
+ }
+
+ @Override
+ public SkDestroyListener makeSkDestroyListener(
+ IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ return mSkDestroyListener;
+ }
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
}
@After
@@ -2166,6 +2189,71 @@
}
@Test
+ public void testAdoptFastDataInput_featureDisabled() throws Exception {
+ // Boot through serviceReady() with flag disabled, verify the persistent
+ // counters are not increased.
+ mFastDataInputTargetAttempts = 0;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ assertEquals(0, mDeps.getCompareStatsInvocation());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterFail() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterSuccess() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(1).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_hasDiff() throws Exception {
+ // Boot through serviceReady() with flag enabled and assumes the stats are
+ // failed to compare, verify the fallbacks counter is increased.
+ mockDefaultSettings();
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mFastDataInputTargetAttempts = 1;
+ mCompareStatsResult = "Has differences";
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter).set(1);
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noDiff() throws Exception {
+ // Boot through serviceReady() with target attempts increased,
+ // assumes there was a previous failure,
+ // and assumes the stats are successfully compared,
+ // verify the successes counter is increased.
+ mFastDataInputTargetAttempts = 2;
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mCompareStatsResult = null;
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter).set(1);
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
public void testStatsFactoryRemoveUids() throws Exception {
// pretend that network comes online
mockDefaultSettings();
@@ -2230,7 +2318,8 @@
final DropBoxManager dropBox = mock(DropBoxManager.class);
return new NetworkStatsRecorder(new FileRotator(
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError,
+ false /* useFastDataInput */, directory);
}
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 30aeca5..6a5ea4b 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,11 +2,14 @@
"presubmit": [
{
"name": "CtsThreadNetworkTestCases"
+ },
+ {
+ "name": "ThreadNetworkUnitTests"
}
],
"postsubmit": [
{
- "name": "ThreadNetworkUnitTests"
+ "name": "ThreadNetworkIntegrationTests"
}
]
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 89dcd39..a9da8d6 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -38,6 +38,8 @@
void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 34b0b06..b5699a9 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,6 +31,7 @@
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -499,6 +500,31 @@
}
}
+ /**
+ * Sets to use a specified test network as the upstream.
+ *
+ * @param testNetworkInterfaceName The name of the test network interface. When it's null,
+ * forbids using test network as an upstream.
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ @VisibleForTesting
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setTestNetworkAsUpstream(
+ testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 60c97bf..1d8cd73 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -53,14 +53,15 @@
import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
-import android.net.MulticastRoutingConfig;
import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
@@ -69,6 +70,7 @@
import android.net.NetworkRequest;
import android.net.NetworkScore;
import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -91,12 +93,12 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.OtDaemonState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import java.io.IOException;
import java.net.Inet6Address;
@@ -147,9 +149,10 @@
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
- private final NetworkRequest mUpstreamNetworkRequest;
+ private NetworkRequest mUpstreamNetworkRequest;
+ private UpstreamNetworkCallback mUpstreamNetworkCallback;
+ private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
- private final LocalNetworkConfig mLocalNetworkConfig;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -168,16 +171,7 @@
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
- mUpstreamNetworkRequest =
- new NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build();
- mLocalNetworkConfig =
- new LocalNetworkConfig.Builder()
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
}
@@ -237,6 +231,60 @@
LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
+ private NetworkRequest newUpstreamNetworkRequest() {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+
+ if (mUpstreamTestNetworkSpecifier != null) {
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
+ .build();
+ }
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
+
+ private LocalNetworkConfig newLocalNetworkConfig() {
+ return new LocalNetworkConfig.Builder()
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ }
+
+ @Override
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
+ }
+
+ private void setTestNetworkAsUpstreamInternal(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ checkOnHandlerThread();
+
+ TestNetworkSpecifier testNetworkSpecifier = null;
+ if (testNetworkInterfaceName != null) {
+ testNetworkSpecifier = new TestNetworkSpecifier(testNetworkInterfaceName);
+ }
+
+ if (!Objects.equals(mUpstreamTestNetworkSpecifier, testNetworkSpecifier)) {
+ cancelRequestUpstreamNetwork();
+ mUpstreamTestNetworkSpecifier = testNetworkSpecifier;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+ requestUpstreamNetwork();
+ sendLocalNetworkConfig();
+ }
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
private void initializeOtDaemon() {
try {
getOtDaemon();
@@ -289,45 +337,63 @@
}
private void requestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback != null) {
+ throw new AssertionError("The upstream network request is already there.");
+ }
+ mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback(
- mUpstreamNetworkRequest,
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- Log.i(TAG, "onAvailable: " + network);
- }
+ mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+ }
- @Override
- public void onLost(@NonNull Network network) {
- Log.i(TAG, "onLost: " + network);
- }
+ private void cancelRequestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback == null) {
+ throw new AssertionError("The upstream network request null.");
+ }
+ mNetworkToInterface.clear();
+ mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+ mUpstreamNetworkCallback = null;
+ }
- @Override
- public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
- Log.i(
- TAG,
- String.format(
- "onLinkPropertiesChanged: {network: %s, interface: %s}",
- network, linkProperties.getInterfaceName()));
- mNetworkToInterface.put(network, linkProperties.getInterfaceName());
- if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
- }
- }
- },
- mHandler);
+ private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onAvailable: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onLost: " + network);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(
+ @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ String.format(
+ "onLinkPropertiesChanged: {network: %s, interface: %s}",
+ network, linkProperties.getInterfaceName()));
+ mNetworkToInterface.put(network, linkProperties.getInterfaceName());
+ if (network.equals(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
}
private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
@Override
public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
Log.i(TAG, "onAvailable: Thread network Available");
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+ checkOnHandlerThread();
Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
if (localNetworkInfo.getUpstreamNetwork() == null) {
mUpstreamNetwork = null;
@@ -370,7 +436,7 @@
TAG,
netCaps,
mLinkProperties,
- mLocalNetworkConfig,
+ newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {};
@@ -754,20 +820,9 @@
if (mNetworkAgent == null) {
return;
}
- final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- LocalNetworkConfig localNetworkConfig =
- configBuilder
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(
- TAG,
- "Sent localNetworkConfig with upstreamConfig "
- + mUpstreamMulticastRoutingConfig
- + " downstreamConfig"
- + mDownstreamMulticastRoutingConfig);
+ Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
private void handleMulticastForwardingStateChanged(boolean isEnabled) {
@@ -800,8 +855,8 @@
MulticastRoutingConfig newDownstreamConfig;
MulticastRoutingConfig.Builder builder;
- if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
- MulticastRoutingConfig.FORWARD_SELECTED) {
+ if (mDownstreamMulticastRoutingConfig.getForwardingMode()
+ != MulticastRoutingConfig.FORWARD_SELECTED) {
Log.e(
TAG,
"Ignore multicast listening address updates when downstream multicast "
@@ -809,8 +864,8 @@
// Don't update the address set if downstream multicast forwarding is disabled.
return;
}
- if (isAdded ==
- mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
+ if (isAdded
+ == mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
return;
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..405fb76
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,55 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "ThreadNetworkIntegrationTestsDefaults",
+ min_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "guava",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ "testables",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+}
+
+android_test {
+ name: "ThreadNetworkIntegrationTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults"
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ compile_multilib: "both",
+}
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..a347654
--- /dev/null
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.thread.tests.integration">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
+ network. Since R shell application don't have such permission, grant permission to the test
+ here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
+ obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.thread.tests.integration"
+ android:label="Thread integration tests">
+ </instrumentation>
+</manifest>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
new file mode 100644
index 0000000..5d3818a
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.thread.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.IntegrationTestUtils.waitForStateAnyOf;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TestNetworkTracker;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Integration test cases for Thread Border Routing feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BorderRoutingTest {
+ private static final String TAG = BorderRoutingTest.class.getSimpleName();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkManager mThreadNetworkManager =
+ mContext.getSystemService(ThreadNetworkManager.class);
+ private ThreadNetworkController mThreadNetworkController;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+
+ // 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");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() throws Exception {
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ var threadControllers = mThreadNetworkManager.getAllThreadNetworkControllers();
+ assertEquals(threadControllers.size(), 1);
+ mThreadNetworkController = threadControllers.get(0);
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(1);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ MoreExecutors.directExecutor(),
+ v -> {
+ latch.countDown();
+ });
+ latch.await();
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(2);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ null, MoreExecutors.directExecutor(), v -> latch.countDown());
+ mThreadNetworkController.leave(
+ MoreExecutors.directExecutor(), v -> latch.countDown());
+ latch.await(10, TimeUnit.SECONDS);
+ });
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+
+ @Test
+ public void infraDevicePingTheadDeviceOmr_Succeeds() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // BR forms a network.
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mThreadNetworkController.join(
+ DEFAULT_DATASET, MoreExecutors.directExecutor(), result -> {});
+ });
+ waitForStateAnyOf(
+ mThreadNetworkController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
+
+ // Creates a Full Thread Device (FTD) and lets it join the network.
+ FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), 10 /* timeoutSeconds */);
+ waitFor(() -> ftd.getOmrAddress() != null, 60 /* timeoutSeconds */);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+
+ // Creates a infra network device.
+ TapPacketReader infraNetworkReader =
+ newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ InfraNetworkDevice infraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
+ infraDevice.runSlaac(60 /* timeoutSeconds */);
+ assertNotNull(infraDevice.ipv6Addr);
+
+ // Infra device sends an echo request to FTD's OMR.
+ infraDevice.sendEchoRequest(ftdOmr);
+
+ // Infra device receives an echo reply sent by FTD.
+ assertNotNull(
+ readPacketFrom(
+ infraNetworkReader,
+ p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
new file mode 100644
index 0000000..01638f3
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
@@ -0,0 +1,180 @@
+/*
+ * 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;
+
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that launches and controls a simulation Full Thread Device (FTD).
+ *
+ * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
+ * and output. See <a
+ * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
+ * available commands.
+ */
+public final class FullThreadDevice {
+ private final Process mProcess;
+ private final BufferedReader mReader;
+ private final BufferedWriter mWriter;
+
+ private ActiveOperationalDataset mActiveOperationalDataset;
+
+ /**
+ * Constructs a {@link FullThreadDevice} for the given node ID.
+ *
+ * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
+ * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
+ * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
+ *
+ * @param nodeId the node ID for the simulation Full Thread Device.
+ * @throws IllegalStateException the node ID is already occupied by another simulation Thread
+ * device.
+ */
+ public FullThreadDevice(int nodeId) {
+ try {
+ mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+ }
+ mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
+ mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+ mActiveOperationalDataset = null;
+ }
+
+ /**
+ * Returns an OMR (Off-Mesh-Routable) address on this device if any.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the first
+ * address which is neither link-local nor mesh-local.
+ */
+ public Inet6Address getOmrAddress() {
+ List<String> addresses = executeCommand("ipaddr");
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ if (address.startsWith("fe80:")) {
+ continue;
+ }
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (!meshLocalPrefix.contains(addr)) {
+ return addr;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Joins the Thread network using the given {@link ActiveOperationalDataset}.
+ *
+ * @param dataset the Active Operational Dataset
+ */
+ public void joinNetwork(ActiveOperationalDataset dataset) {
+ mActiveOperationalDataset = dataset;
+ executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
+ executeCommand("ifconfig up");
+ executeCommand("thread start");
+ }
+
+ /** Stops the Thread network radio. */
+ public void stopThreadRadio() {
+ executeCommand("thread stop");
+ executeCommand("ifconfig down");
+ }
+
+ /**
+ * Waits for the Thread device to enter the any state of the given {@link List<String>}.
+ *
+ * @param states the list of states to wait for. Valid states are "disabled", "detached",
+ * "child", "router" and "leader".
+ * @param timeoutSeconds the number of seconds to wait for.
+ */
+ public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> states.contains(getState()), timeoutSeconds);
+ }
+
+ /**
+ * Gets the state of the Thread device.
+ *
+ * @return a string representing the state.
+ */
+ public String getState() {
+ return executeCommand("state").get(0);
+ }
+
+ /** Runs the "factoryreset" command on the device. */
+ public void factoryReset() {
+ try {
+ mWriter.write("factoryreset\n");
+ mWriter.flush();
+ // fill the input buffer to avoid truncating next command
+ for (int i = 0; i < 1000; ++i) {
+ mWriter.write("\n");
+ }
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
+ }
+ }
+
+ private List<String> executeCommand(String command) {
+ try {
+ mWriter.write(command + "\n");
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to write the command " + command + " to ot-cli-ftd", e);
+ }
+ try {
+ return readUntilDone();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to read the ot-cli-ftd output of command: " + command, e);
+ }
+ }
+
+ private List<String> readUntilDone() throws IOException {
+ ArrayList<String> result = new ArrayList<>();
+ String line;
+ while ((line = mReader.readLine()) != null) {
+ if (line.equals("Done")) {
+ break;
+ }
+ if (line.startsWith("Error:")) {
+ fail("ot-cli-ftd reported an error: " + line);
+ }
+ if (!line.startsWith("> ")) {
+ result.add(line);
+ }
+ }
+ return result;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
new file mode 100644
index 0000000..43a800d
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+import static android.net.thread.IntegrationTestUtils.getRaPios;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.testutils.TapPacketReader;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that simulates a device on the infrastructure network.
+ *
+ * <p>This class directly interacts with the TUN interface of the test network to pretend there's a
+ * device on the infrastructure network.
+ */
+public final class InfraNetworkDevice {
+ // The MAC address of this device.
+ public final MacAddress macAddr;
+ // The packet reader of the TUN interface of the test network.
+ public final TapPacketReader packetReader;
+ // The IPv6 address generated by SLAAC for the device.
+ public Inet6Address ipv6Addr;
+
+ /**
+ * Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
+ * TapPacketReader}.
+ *
+ * @param macAddr the MAC address of the device
+ * @param packetReader the packet reader of the TUN interface of the test network.
+ */
+ public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+ this.macAddr = macAddr;
+ this.packetReader = packetReader;
+ }
+
+ /**
+ * Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
+ *
+ * @param dstAddr the destination address of the packet.
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
+ ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
+ packetReader.sendResponse(icmp6Packet);
+ }
+
+ /**
+ * Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
+ *
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendRsPacket() throws IOException {
+ ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
+ ByteBuffer rs =
+ Ipv6Utils.buildRsPacket(
+ (Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
+ IPV6_ADDR_ALL_ROUTERS_MULTICAST,
+ slla);
+ packetReader.sendResponse(rs);
+ }
+
+ /**
+ * Runs SLAAC to generate an IPv6 address for the device.
+ *
+ * <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
+ * address if there's any available Prefix Information Option (PIO). For now it only generates
+ * one address in total and doesn't track the expiration.
+ *
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
+ */
+ public void runSlaac(int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> (ipv6Addr = runSlaac()) != null, timeoutSeconds, 5 /* intervalSeconds */);
+ }
+
+ private Inet6Address runSlaac() {
+ try {
+ sendRsPacket();
+
+ final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+
+ final List<PrefixInformationOption> options = getRaPios(raPacket);
+
+ for (PrefixInformationOption pio : options) {
+ if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
+ final byte[] addressBytes = pio.prefix;
+ addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+ addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+ return (Inet6Address) InetAddress.getByAddress(addressBytes);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to generate an address by SLAAC", e);
+ }
+ return null;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
new file mode 100644
index 0000000..9d9a4ff
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
@@ -0,0 +1,221 @@
+/*
+ * 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;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+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.util.concurrent.MoreExecutors.directExecutor;
+
+import android.net.TestNetworkInterface;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+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.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+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 {
+ private IntegrationTestUtils() {}
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every second.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException if the condition is not met after the timeout.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
+ throws TimeoutException {
+ waitFor(condition, timeoutSeconds, 1);
+ }
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every {@code intervalSeconds}.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @param intervalSeconds the period to check the {@code condition}.
+ * @throws TimeoutException if the condition is still not met when the timeout expires.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds, int intervalSeconds)
+ throws TimeoutException {
+ for (int i = 0; i < timeoutSeconds; i += intervalSeconds) {
+ if (condition.get()) {
+ return;
+ }
+ SystemClock.sleep(intervalSeconds * 1000L);
+ }
+ if (condition.get()) {
+ return;
+ }
+ throw new TimeoutException(
+ String.format(
+ "The condition failed to become true in %d seconds.", timeoutSeconds));
+ }
+
+ /**
+ * 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 timeoutSeconds the number of seconds ot wait for.
+ * @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, int timeoutSeconds)
+ throws TimeoutException {
+ SettableFuture<Integer> future = SettableFuture.create();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
+ controller.unregisterStateCallback(callback);
+ return role;
+ } catch (InterruptedException | ExecutionException e) {
+ throw new TimeoutException(
+ String.format(
+ "The device didn't become an expected role in %d seconds.",
+ timeoutSeconds));
+ }
+ }
+
+ /**
+ * Reads 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[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ byte[] packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
+ if (filter.test(packet)) return packet;
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+ return false;
+ }
+ 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;
+ }
+
+ /** 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;
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 2f120b2..75eb043 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -28,11 +28,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
-import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationReceiver;
-import android.net.thread.IOperationalDatasetCallback;
-import android.net.thread.IStateCallback;
-import android.net.thread.IThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.os.Binder;
@@ -111,6 +106,11 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -359,4 +359,27 @@
assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger callbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetTestNetworkAsUpstreamReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setTestNetworkAsUpstream(anyString(), any(IOperationReceiver.class));
+ mController.setTestNetworkAsUpstream(
+ null, Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+ mController.setTestNetworkAsUpstream(
+ new String("test0"), Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ }
}